The simplest way to get spoken words into a text box on a page is to not build a
speech-to-text pipeline at all: the browser already has one. The Web Speech API’s
SpeechRecognition interface is a standard, no build step, no model download, no
server round trip for the recognition itself. This lesson wires it into a plain
JavaScript widget, the same shape Course 11 uses for its chat widget, and is honest
about where and why it does not work everywhere.
A plain JavaScript widget file to add this to, Course 11’s Building the Chat Widget UI lesson is the reference shape this lesson assumes, though any input field works.
Step 1: Feature-detect before doing anything else
SpeechRecognition is exposed with a webkit prefix in Safari and some Chromium
builds, and under its unprefixed name in others. Check for both before using it:
// File: assets/js/voice-input.js
( function () {
var SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
if ( ! SpeechRecognitionCtor ) {
// No SpeechRecognition support at all, hide the mic button entirely
// rather than show a control that silently does nothing.
var micButtons = document.querySelectorAll( '.site-chat-mic' );
micButtons.forEach( function ( btn ) {
btn.hidden = true;
} );
return;
}
initVoiceInput( SpeechRecognitionCtor );
} )();
As of this writing, SpeechRecognition works in Chrome, Edge, and Opera without a
prefix, and in Safari 14.1+ on macOS and 14.5+ on iOS with the webkit prefix.
Firefox ships an implementation behind the dom.webspeech.recognition.enable flag,
disabled by default, so it does not work for most Firefox visitors out of the box.
MDN currently lists SpeechRecognition as “limited availability,” not baseline.
Hiding the mic button when unsupported, as above, is not a workaround, it’s the
correct behavior: never show a control that silently fails.
Step 2: Starting and handling a recognition session
// File: assets/js/voice-input.js (continued)
function initVoiceInput( SpeechRecognitionCtor ) {
var recognition = new SpeechRecognitionCtor();
recognition.lang = document.documentElement.lang || 'en-US';
recognition.interimResults = false;
recognition.maxAlternatives = 1;
var micButton = document.querySelector( '.site-chat-mic' );
var input = document.getElementById( 'site-chat-input' );
var listening = false;
micButton.addEventListener( 'click', function () {
if ( listening ) {
recognition.stop();
return;
}
recognition.start();
} );
recognition.addEventListener( 'start', function () {
listening = true;
micButton.classList.add( 'is-listening' );
micButton.setAttribute( 'aria-label', 'Stop listening' );
} );
recognition.addEventListener( 'result', function ( event ) {
var transcript = event.results[ 0 ][ 0 ].transcript;
input.value = transcript;
input.dispatchEvent( new Event( 'input' ) );
} );
recognition.addEventListener( 'error', function ( event ) {
// 'no-speech' and 'aborted' are routine, anything else is worth logging
if ( event.error !== 'no-speech' && event.error !== 'aborted' ) {
console.error( 'Speech recognition error:', event.error );
}
} );
recognition.addEventListener( 'end', function () {
listening = false;
micButton.classList.remove( 'is-listening' );
micButton.setAttribute( 'aria-label', 'Start voice input' );
} );
}
interimResults = false and maxAlternatives = 1 keep this simple: one final
transcript per recognition session, no partial-results UI to build yet. That’s a
reasonable place to start, and something you can revisit once the basic flow works.
Step 3: Add the mic button to the widget markup
// File: assets/js/chat-widget.js (one line added to Course 11's createWidget())
' <button type="button" class="site-chat-mic" aria-label="Start voice input">🎤</button>' +
' <input id="site-chat-input" type="text" placeholder="Ask a question" autocomplete="off" />' +
Placed inside the same form Course 11 built, next to the text input it fills.
Step 4: What this is not doing
If a site handles sensitive input, healthcare intake forms, legal correspondence, anything under stricter data handling rules, sending every spoken word to a third-party recognition server by default is a real consideration, not a hypothetical one. Lesson 3 covers the local alternative, faster-whisper, for exactly this case.
Test it: confirm recognition actually produces text
Open the widget in Chrome, click the mic button, and say a short sentence out loud.
document.getElementById( 'site-chat-input' ).value
Run that in the console right after speaking, and it should show the words you said,
not an empty string or an error. Then test in Firefox with default settings: the mic
button should be hidden entirely, since SpeechRecognitionCtor will be undefined
there, confirming the feature-detection in Step 1 is doing its job rather than
showing a button that silently fails.
Recap
The Web Speech API’s SpeechRecognition interface gets real speech-to-text working
in a browser with no server-side model of your own, feature-detected with the
webkit prefix Safari and some Chromium builds still require, and hidden entirely
where it is not supported rather than shown broken. Its biggest caveat is that “in the
browser” does not mean “offline” or “private,” Chrome’s implementation is
server-based. That caveat, and Firefox’s default-off support, are exactly why the
next lesson builds a local alternative.
Resources & further reading
- SpeechRecognition interface, MDN Web Docs, developer.mozilla.org
- Web Speech API overview, MDN Web Docs, developer.mozilla.org
- SpeechRecognition browser support, Can I Use, caniuse.com