ckeditor5-voice-input
v0.1.0
Published
Voice-to-text dictation for CKEditor 5, built on the browser's Web Speech API.
Maintainers
Readme
ckeditor5-voice-input
Dictation for CKEditor 5, built on the browser's Web Speech API.
A toolbar button starts and stops recognition. Recognised speech is inserted at the selection
through the editor's own insertText command, so it lands at the caret, never replaces the
document, and joins the regular undo stack. A footer inside the input shows the recognition
state, the phrase currently being heard, and — optionally — a live microphone level meter, so
the speaker can tell whether they are actually being picked up.
There is no service to sign up for and no runtime dependency: recognition is whatever the browser provides.
Requirements
| | |
| --- | --- |
| CKEditor 5 | >=48.0.0 <49 (peer dependency) |
| Browser | one that implements SpeechRecognition — see Browser support |
| Context | a secure context (HTTPS, or localhost) and microphone permission |
The package is ESM-only, as ckeditor5 itself is.
Installation
npm install ckeditor5-voice-inputQuick start
import { ClassicEditor, Essentials, Paragraph } from 'ckeditor5';
import { VoiceInput } from 'ckeditor5-voice-input';
import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-voice-input/index.css';
await ClassicEditor.create( {
attachTo: document.querySelector( '#editor' ),
licenseKey: '<YOUR_LICENSE_KEY>',
plugins: [ Essentials, Paragraph, VoiceInput ],
toolbar: [ 'voiceInput' ]
} );Three things are needed: add VoiceInput to plugins, add 'voiceInput' to toolbar, and
import ckeditor5-voice-input/index.css alongside CKEditor's own stylesheet. Without that
stylesheet the footer renders unstyled — the plugin takes its colours from CKEditor's theme
variables, which ckeditor5/ckeditor5.css defines.
Supported editor types
Verified by the test suite: ClassicEditor and InlineEditor.
The footer positions itself against the editable's container, which those two provide. Other editor types (balloon, decoupled, multi-root) are untested — the plugin may well work, but it is not claimed. If the container cannot be found the footer is simply not mounted; dictation still works and nothing throws.
Configuration
voiceInput?: {
language?: string;
levelMeter?: boolean;
}await ClassicEditor.create( {
// ...
voiceInput: {
language: 'en-GB',
levelMeter: false
}
} );language
A BCP 47 language tag handed to the recognition engine. Defaults to
editor.locale.contentLanguage — dictated text goes into the content, so the content
language is what matters, not the UI language.
| Configured | Used for recognition | |
| --- | --- | --- |
| fa | fa-IR | bare Persian gains a region |
| en | en-US | bare English gains a region |
| fa-IR, en-GB, fr-CA, pt-BR … | unchanged | any tag that already names a region |
| de, ar, ja … | unchanged | no region is invented for you |
The two aliases exist so fa and en behave as they always have. Everything else is passed
through as given, so any language the browser's engine supports is reachable. If the engine
rejects the tag it reports language-not-supported and the footer says so.
levelMeter
Whether to draw the live microphone level meter. Defaults to true.
Setting it to false means the plugin makes no getUserMedia() call of its own and
creates no AudioContext. Dictation is unaffected — only the meter disappears, and the footer
uses the released space for the status text and transcript. See
Privacy for why you might want that.
Configuration is read once when the editor is created. There is no runtime toggle.
Translations
The plugin's 17 user-visible strings go through CKEditor's translation system. English is built in; a Persian dictionary ships with the package, and every other language is present as an empty dictionary awaiting a translator.
import faTranslations from 'ckeditor5-voice-input/translations/fa.js';
await ClassicEditor.create( {
// ...
language: 'fa',
translations: [ faTranslations ]
} );Browser support
Runtime feature detection is authoritative. The plugin looks for
SpeechRecognition (and the webkit-prefixed form) when the editor is created. Where neither
exists the toolbar button is rendered disabled with an explanatory tooltip rather than
hidden, so the feature's absence is visible instead of mysterious.
Availability, and the quality of what you get, depend on the browser and the platform:
- Chromium-based browsers implement it, prefixed for a long time and unprefixed more recently.
- Safari implements the prefixed form.
- Firefox added an implementation, but it sits behind a preference that is disabled by default. A stock Firefox therefore exposes no constructor, and the button appears disabled. Firefox is not presented as supported.
- Mobile support varies, including how much of the API's configuration actually takes effect.
Consult MDN's compatibility data rather than this file for the current picture.
Privacy
Three separate things, worth keeping apart.
Recognition itself. Where speech is turned into text is decided by the browser and the platform, not by this plugin, and it may involve a remote recognition service. The plugin calls a standard browser API and cannot guarantee where processing happens or what a given implementation transmits. Some implementations additionally offer on-device recognition; this plugin does not currently opt into it. If where recognition happens matters for your users, verify it for the browsers you support before deploying.
The level meter. Separately from recognition, and only when levelMeter is enabled, the
plugin calls getUserMedia() itself to draw the microphone level. That is a second capture,
in addition to whatever recognition opens, and it is why a browser may show the microphone
indicator. levelMeter: false prevents that plugin-side capture entirely while leaving
dictation working.
The diagnostic trace. Off unless you deliberately arm it — see Troubleshooting.
Persian text normalisation
The plugin is not Persian-specific; it accepts arbitrary recognition language tags. But when
the base recognition language is Persian (fa, fa-IR, fa-AF, …) recognised text is
normalised before insertion, because a recogniser may return Arabic letterforms that look
identical to the Persian ones but are different characters — enough to break search, sorting
and font fallback against the rest of a Persian document.
What it does: maps Arabic kaf, yeh and alef maksura to the Persian keheh and farsi yeh; maps Arabic-Indic digits to Persian digits; strips harakat and tatweel; collapses repeated zero-width non-joiners and trims them from the edges; and uses Persian punctuation for the comma and question mark that auto-punctuation inserts.
What it deliberately does not do: insert zero-width non-joiners (doing it correctly needs a verb dictionary, and getting it wrong is worse than leaving it alone); convert ASCII digits, which would corrupt URLs, version numbers and identifiers; fold correctly-spelled Arabic letters a Persian document may legitimately quote; or touch a comma sitting between two ASCII digits, which is a thousands separator rather than a clause break.
For every other language the transcript is inserted exactly as recognised.
Theming
The footer takes its surface, borders, text colour, accent and error colours from CKEditor's own theme variables, so it follows the editor rather than importing a palette. You should not need to override anything for the plugin's sake — theme the editor and the footer follows.
Three colours have no CKEditor counterpart and are the ones worth customising:
| Custom property | Purpose |
| --- | --- |
| --ck-voice-input-dot-live-coordinates | HSL components of the recording dot while the microphone is live. Deliberately not CKEditor's error colour, so "recording" stays distinguishable from "something went wrong". |
| --ck-voice-input-dot-waiting-coordinates | HSL components of the dot while the engine is being waited on — connecting, recognising, reconnecting. CKEditor ships no warning colour. |
| --ck-voice-input-text-muted | Colour of the status label and interim transcript. Defaults to the theme's own text colour, softened by --ck-voice-input-muted-opacity. |
Two layout properties are also exposed: --ck-voice-input-meter-height and
--ck-voice-input-reserved-space (the room kept clear at the bottom of the editable while
dictation runs).
:root {
--ck-voice-input-dot-live-coordinates: 340, 80%, 55%;
--ck-voice-input-text-muted: #4a5568;
}The dot's blink is the only animation, and it is suppressed under
prefers-reduced-motion: reduce. State is never communicated by colour alone — the
translated status label is the primary signal, with the dot and meter as supplements.
Supported imports
// The plugin and its public types.
import {
VoiceInput,
type VoiceInputConfig,
type VoiceInputState,
type VoiceInputErrorCode,
type VoiceInputHint
} from 'ckeditor5-voice-input';
// The stylesheet.
import 'ckeditor5-voice-input/index.css';
// A translation dictionary.
import faTranslations from 'ckeditor5-voice-input/translations/fa.js';That is the whole supported surface. Everything else inside the package is an implementation detail and is not reachable as a package subpath.
To drive dictation programmatically, take the command from CKEditor's command collection — it is fully typed, and there is no need to construct anything:
const command = editor.commands.get( 'voiceInput' );
command?.start();
command?.stop();
command?.setLanguage( 'en-GB' );
// Observables: `state`, `errorCode`, `hint`, `interim`, `language`, `value`.
command?.on( 'change:state', () => console.log( command.state, command.errorCode ) );Troubleshooting
When dictation misbehaves the Web Speech API usually reports nothing useful — a broken session looks like a healthy one. The plugin can record its own event timeline instead.
Arm it, then reload and reproduce the problem. The reload is required: the flag is read once and cached.
sessionStorage.setItem( 'ckeditor5-voice-input:trace-enabled', '1' );
location.reload();Read it back in the same page load, before navigating away — the records live in memory on
window, not in storage, and are lost on navigation:
console.table( window[ 'ckeditor5-voice-input:trace' ] );Turn it off:
sessionStorage.removeItem( 'ckeditor5-voice-input:trace-enabled' );The trace is designed not to include dictated text, audio, or document content. It contains recognition state and timing information, configured language tags, retry counters, and platform error codes; a transcript appears only as a character count. Review it before sharing it in a bug report. It holds at most 4000 records and then stops recording. Nothing is logged to the console, armed or not.
Known limitations
- Firefox ships its implementation behind a disabled-by-default preference, so a stock Firefox cannot dictate. The button is disabled and says so.
- One microphone per page. Browsers run a single recognition session at a time, so with several editors on one page, starting dictation in one stops it in another. That is deliberate — the alternative is two sessions destroying each other.
- The GPL "Powered by" badge may overlap the footer. In GPL builds CKEditor renders a floating badge near the bottom-right of the focused editable, which can sit over the right edge of the voice-input footer — the area holding the interim transcript. The badge is positioned by CKEditor outside the editor's own container, so the footer has no reliable, non-internal way to avoid it, and depending on CKEditor's private DOM to do so was deliberately rejected. Builds with a commercial licence key are unaffected.
- Editor types other than classic and inline are untested; see Supported editor types.
- On-device recognition (where some implementations can keep audio and transcript on the machine) is not yet opted into.
Contributing
See CONTRIBUTING.md for the development requirements and scripts.
License
MIT.
CKEditor 5 is a peer dependency and is not redistributed by this package; it carries its own licensing terms.
