Speech-to-Text

Speech-to-Text in the Browser With the Web Speech API

⏱ 15 min

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.

What you'll learn in this lesson
Feature-detecting SpeechRecognition correctly
Including the webkit-prefixed form Safari and Chromium browsers use.
Starting and stopping a recognition session
Handling the result, error, and end events a real widget needs.
What "in the browser" does not mean here
Chrome's implementation sends audio to a server for processing, so this is not automatically private or offline.
Real current browser support
Which browsers support it today, and Firefox's current status, so you can build a sane fallback.
Prerequisites

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 );
} )();
Current real support is not universal

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

Deliberately out of scope for this lesson
1
No offline guarantee
Chrome's SpeechRecognition implementation sends audio to a Google server for processing. It is not a local, private pipeline, even though the API surface is entirely in-browser JavaScript.
2
No automatic submit yet
The transcript lands in the input field, Lesson 5 wires up auto-submit as part of the full voice-driven widget.
3
No text-to-speech reply
This lesson is speech-to-text only. SpeechSynthesis, the output half of the Web Speech API, and Piper as a local alternative, are Lesson 4.
Why this matters for a privacy-sensitive site

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

← What Voice for WordPress Actually Means Today Local, Private Speech-to-Text With Whisper →