Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Building UI with AI agents
  4. React Speech to Text Input: Mic, Waveform, Fallback
Auf Deutsch lesen
  1. Blog
  2. Building UI with AI agents
  3. React Speech to Text Input: Mic, Waveform, Fallback
Building UI with AI agents
Building UI with AI agents

React Speech to Text Input: Mic, Waveform, Fallback

SR
Serban Rusu · Founder of Wingo UI
Oct 16, 2026 · 9 min read

On this page

0%
  1. What does a good speech to text input look like?
  2. How does the Web Speech API work in React, and where does it break?
  3. How do I get the audio Blob and the transcript in React?
  4. How do I add a dictation button in React to any text field?
  5. How do I fall back to server transcription?
  6. How does voice input fit a chat UI?
  7. How do you play back a voice message with its waveform?
  8. How do I make voice input accessible on keyboards, screen readers and phones?
  9. What are the common speech to text pitfalls?
  10. Where to go next
  11. FAQ
    1. Does the Web Speech API work in every browser?
    2. Is browser speech recognition private or offline?
    3. How do I send recorded audio to a speech to text API from React?
    4. Why does my Web Speech API transcript stop after a pause?
    5. Is the Wingo UI Voice Input free?

TL;DR

A React speech to text input records with MediaRecorder and shows live words from the Web Speech API where the browser has it: Chrome and Safari do, Firefox does not by default, and Chrome sends the audio to a server for recognition. Treat the browser transcript as a preview, keep the audio Blob, and send it to a transcription endpoint when the browser gives you no words or you need the same result everywhere.

Published Oct 16, 2026

A React speech to text input looks like one button: tap the mic, talk, get text. Building one that holds up takes more. The browser has to ask for the microphone at the right moment, the person needs to see that it is listening, the words should appear as they speak, and the whole thing has to keep working in Firefox, in a Home Screen app on iOS and when the recognition service drops. This guide covers each of those with the code of the Wingo UI Voice Input, the mic that grows into a recording pill with a live waveform. We build that library, so we are not neutral; browser facts link to their sources.

bash
npx wingo-ui@latest add voice-input

What does a good speech to text input look like?

A mic button at rest, a recording pill while you talk, the text when you are done. Voice Input has three shapes behind variant:

  • button (default): a round mic for composers and toolbars. On tap it grows on a spring into a pill with Cancel, a pulsing red dot, a waveform, a timer and Send, then shrinks back into the mic.
  • bar: a full-width bar that is always visible, the messaging-app layout, with the transcript above it.
  • orb: a voice mode built on the Matrix Orb that listens, thinks while your promise runs and blooms while the app talks back.

The live words sit above the pill, four lines tall, with the words the recognizer is still unsure of muted. The recording dot stays red whatever tone you pick, because red means recording.

Tap the mic and allow the microphone: it grows into a pill with a waveform that follows your voice. Talk, then press Send or Escape to cancel
$ npx wingo-ui@latest add voice-input
ProVoice Input docs

How does the Web Speech API work in React, and where does it break?

A Web Speech API React hook is short. It wraps SpeechRecognition: create a recognizer, set continuous and interimResults, read onresult, stop on unmount. It breaks in the browsers and modes around it:

BrowserSpeechRecognitionNotes
Chrome desktopYes, webkit prefix since 33; MDN lists the unprefixed name from 139Audio goes to a server for recognition
Safari 14.1+ (macOS, iOS)Yes, webkitSpeechRecognitionNeeds Siri or Dictation turned on; not in Home Screen web apps
FirefoxNoOff by default; Can I use lists it as disabled
EdgeUnclearMDN's data mirrors Chrome; Can I use lists it as unsupported because no events appear to fire. Test it yourself

Sources, checked October 2026: MDN SpeechRecognition (opens in a new tab), MDN browser compat data (opens in a new tab), Can I use (opens in a new tab) and WebKit bug 225298 (opens in a new tab), where the reply from WebKit says the API is not available in SafariViewController or Home Screen web apps "for now".

Three more limits shape the design:

  1. It is not private by default. MDN says that in Chrome "your audio is sent to a web service for recognition processing, so it won't work offline." Chrome 139 added an experimental processLocally option and SpeechRecognition.available() / install() for on-device language packs on desktop; Android and Safari do not have them in MDN's data.
  2. Chrome stops on its own. A continuous run ends after a pause, onend fires and the transcript stops growing. Voice Input restarts the recognizer from onend while you are still recording and joins the finished text of each run.
  3. It needs a gesture and HTTPS. Browsers open the microphone only from a tap or a key press, and on plain HTTP navigator.mediaDevices does not exist. Voice Input asks for permission after the first tap, never on page load, and shows "Voice input isn't available in this browser." when it cannot record at all.

So treat the browser transcript as a fast preview. The audio is the source of truth.

How do I get the audio Blob and the transcript in React?

When the person presses Send, onRecordingComplete receives { blob, mimeType, durationMs, transcript }. The audio comes from MediaRecorder, which picks the first type the browser can record: audio/webm;codecs=opus in Chrome, audio/mp4 in Safari versions without WebM recording. The transcript comes from the browser recognizer, or from your transcript prop when you stream words from your own service. Turn each source on or off:

  • audio={false}: no recording, only words and length. Good for plain dictation.
  • transcribe="none": no browser recognizer, for server transcription only.
  • lang="en-US": the recognition language. Empty falls back to the locale prop, the UIProvider locale, then the page's <html lang> and the browser language.

Return a promise and the pill shows a spinner until it settles. If it rejects, the error line appears with Try again, which sends the same recording again.

How do I add a dictation button in React to any text field?

This is the dictation button React forms usually want: words only, appended to what is already typed.

tsx
"use client"
import { useState } from "react"
import { Textarea } from "@/components/ui/textarea"
import { VoiceInput } from "@/components/ui/voice-input"
export function DictatedNote() {
const [note, setNote] = useState("")
return (
<div className="flex flex-col gap-2">
<Textarea label="Note" value={note} onValueChange={setNote} minRows={4} />
<div className="flex justify-end">
<VoiceInput
audio={false}
lang="en-US"
maxDuration={60}
onRecordingComplete={({ transcript }) => {
if (transcript) setNote((text) => [text, transcript].filter(Boolean).join(" "))
}}
/>
</div>
</div>
)
}

onTranscript(text, { final }) fires while the person talks, if you want the words to land in the field live. In Firefox this version records nothing useful, because there is no recognizer and no audio. The next section fixes that.

How do I fall back to server transcription?

Keep the audio, and when the browser hands you no words, send the Blob to your own endpoint. The client stays the same in every browser:

tsx
"use client"
import { VoiceInput, type VoiceInputResult } from "@/components/ui/voice-input"
async function transcribeOnServer(result: VoiceInputResult) {
if (!result.blob) return ""
// chrome records audio/webm, safari can record audio/mp4: name the file after it
const ext = result.mimeType?.includes("mp4") ? "mp4" : "webm"
const body = new FormData()
body.append("audio", result.blob, `dictation.${ext}`)
const response = await fetch("/api/transcribe", { method: "POST", body })
if (!response.ok) throw new Error("Transcription failed")
const { text } = (await response.json()) as { text: string }
return text
}
export function Dictation({ onText }: { onText: (text: string) => void }) {
return (
<VoiceInput
lang="en-US"
maxDuration={120}
onRecordingComplete={async (result) => {
// the browser preview when it has one, the server otherwise
onText(result.transcript || (await transcribeOnServer(result)))
}}
/>
)
}

The route handler keeps the API key on the server. This one uses OpenAI's transcription endpoint; any provider that takes a file works the same way.

ts
// app/api/transcribe/route.ts
import OpenAI from "openai"
const openai = new OpenAI()
export async function POST(request: Request) {
const form = await request.formData()
const audio = form.get("audio")
if (!(audio instanceof File)) return Response.json({ error: "No audio" }, { status: 400 })
const transcription = await openai.audio.transcriptions.create({
file: audio,
model: "gpt-transcribe",
})
return Response.json({ text: transcription.text })
}

As of October 2026, OpenAI's speech to text guide (opens in a new tab) lists gpt-transcribe for recorded speech, a 25 MB upload limit and webm, mp4, m4a, wav and mp3 among the accepted formats, so both browsers' recordings go in as they are. maxDuration (120 seconds by default) keeps a forgotten recording from growing past what you want to upload.

If you want the same words in every browser, set transcribe="none" and always use the server. The pill then says Transcribing while your promise runs.

How does voice input fit a chat UI?

In a voice input chat UI the mic lives in the composer. The Prompt Input does that with one prop: voice adds the mic to the toolbar, the pill takes the toolbar while recording, and the words go in at the caret, replacing a selection. Cancel puts the text back as it was.

tsx
"use client"
import { PromptInput } from "@/components/ui/prompt-input"
export function Composer({ send }: { send: (text: string) => Promise<void> }) {
return (
<PromptInput
voice
voiceInputProps={{ lang: "en-US", maxDuration: 60, mode: "hold" }}
onSubmit={({ text }) => send(text)}
/>
)
}

Pass onVoice as well and the mic sends voice messages, { blob, mimeType, durationMs, transcript }, instead of dictating. The React prompt input guide covers the rest of the composer, and the React AI chat UI guide covers the thread around it.

Tap the mic in the toolbar and talk: the pill replaces the toolbar and the words go in at the caret. Press Enter to send
$ npx wingo-ui@latest add prompt-input
ProPrompt Input docs

How do you play back a voice message with its waveform?

A React voice recorder waveform has two jobs: live while recording, still once the message is sent. While recording, Voice Input reads the microphone through an analyser and writes the bar transforms straight to the DOM in a requestAnimationFrame loop, never through React state, and pauses it off screen. The same bars ship as VoiceWaveform for your own layouts; give it values for a still waveform of a recorded note.

For playback, the Audio Player has a compact variant made for chat bubbles: play, bars you can scrub, time and a speed chip. Without peaks it decodes the file with Web Audio once it is on screen, so an object URL from the Blob gets its real waveform.

tsx
"use client"
import { useEffect, useState } from "react"
import { AudioPlayer } from "@/components/ui/audio-player"
export function VoiceMessage({ blob }: { blob: Blob }) {
const [src, setSrc] = useState("")
// one object url per blob, revoked when the bubble goes away
useEffect(() => {
const url = URL.createObjectURL(blob)
setSrc(url)
return () => URL.revokeObjectURL(url)
}, [blob])
return src ? <AudioPlayer variant="compact" src={src} /> : null
}

Once the upload finishes, swap the object URL for the stored file's URL and pass peaks if your server already computed them.

Play the voice message, then drag across the bars to scrub and tap the speed chip
$ npx wingo-ui@latest add audio-player
ProAudio Player docs

How do I make voice input accessible on keyboards, screen readers and phones?

Give the mic a real button, announce state changes once and size every target for a thumb. Voice Input does each of these:

  • Keyboard. Tab to the mic and press Enter or Space. Focus moves to Send while recording, and Escape cancels.
  • Screen readers. They hear Listening, Transcribing and the recorded length once, never the ticking timer.
  • Phones. Every target is 44px on touch. mode="hold" gives push to talk: hold, let go to send, slide more than 80px toward the start to arm cancel, with a haptic tick (the Vibration API post explains where that works). The held mic sets touch-action: none so the page does not scroll under your thumb.
  • Mis-taps. A recording shorter than minDuration (500 ms by default) is dropped, and a hint asks for a longer message.
  • Errors. A blocked or missing microphone explains itself in place with Try again.
  • Reduced motion. The morph becomes a cross-fade and the waveform a still level meter.

What are the common speech to text pitfalls?

Most bugs come from the permission prompt, leaked microphone streams and testing in one browser.

  • Asking for the microphone on page load. People deny a prompt they did not ask for, and a denial sticks. Ask on the first tap.
  • Trusting the interim text. Interim words change. Store the final transcript, or the server's.
  • Forgetting the tracks. Stop every MediaStream track, audio context and recognizer on stop, cancel and unmount, or the browser's recording indicator stays on. The useVoiceRecorder hook does this if you build a custom UI on the same engine.
  • One file extension for all browsers. A Safari recording named .webm confuses some APIs. Name it after the mime type.
  • Two mics on one page. A composer mic and a voice-note mic can both start. In Voice Input a second recorder that starts stops and sends the first, so only one stream holds the microphone.
  • Testing only in Chrome. Try Firefox, Safari with Dictation off and a Home Screen install before you ship.

Where to go next

Open the Voice Input docs, switch variant, mode and transcribe in the playground, then install it with npx wingo-ui@latest add voice-input. Voice Input, Prompt Input and Audio Player are part of Wingo UI Pro. If a coding agent builds your screens, the component library MCP guide shows how to point it at these components, and more posts live in Building UI with AI agents.

Components in this post

  • Voice Input

    A React voice input: a mic that grows into a recording pill with a live waveform and transcript, a full-width bar, or a voice orb.

    Pro
  • Prompt Input

    A React prompt input for AI chat: a growing text box with attachments, tool chips, a model picker, dictation and a send and stop button.

    Pro
  • Audio Player

    A React audio player with a waveform you can see and scrub, as a podcast card, a chat voice message or a thin call-log bar.

    Pro

FAQ

Does the Web Speech API work in every browser?

No. MDN lists SpeechRecognition as limited availability: Chrome and Safari 14.1+ ship it (Safari with the webkit prefix), Firefox keeps it off by default, and Safari does not offer it in Home Screen web apps. Feature detect both SpeechRecognition and webkitSpeechRecognition and keep a fallback.

Is browser speech recognition private or offline?

Not by default. MDN notes that in Chrome the audio is sent to a web service for recognition, so it does not work offline. Chrome 139 added an experimental processLocally option on desktop, but you should not rely on it yet.

How do I send recorded audio to a speech to text API from React?

Record with MediaRecorder, put the Blob in a FormData body and POST it to your own route handler, which forwards it to the transcription API with the server-side key. Pick the file extension from the recorder's mime type, since Chrome records audio/webm and Safari can record audio/mp4.

Why does my Web Speech API transcript stop after a pause?

Chrome ends a continuous recognition run on its own after silence, and the onend event fires. Restart it from onend while the user is still recording, and join the finished text of each run so nothing is lost.

Is the Wingo UI Voice Input free?

No. Voice Input, Prompt Input and Audio Player are part of Wingo UI Pro. As of October 2026, the 75 free items include the Button, Tooltip, Spinner and Matrix Orb that Voice Input builds on.

  • Voice input
  • Speech to text
  • AI chat
  • React
  • Mobile UI

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Building UI with AI agents

React Prompt Input: Enter, IME, Files, Stop and Voice

A React prompt input that handles Enter vs Shift+Enter, IME composition, uploads with progress, a Stop button, dictation and the iPhone keyboard. With code.

SRSerban Rusu·Oct 16, 2026·10 min read
Component guides

React Audio Player with Waveform: Scrub, Seek, Peaks

Build a React audio player with waveform scrubbing for podcasts, chat voice notes and call logs, with keyboard seek, precomputed peaks and no layout shift.

SRSerban Rusu·Oct 19, 2026·9 min read
Building UI with AI agents

AI Elements Alternative: 5 React AI Chat Kits Compared

Looking for an AI Elements alternative before its November 2026 deprecation? Five React AI chat kits compared on streaming, tool calls, phones and lock-in.

SRSerban Rusu·Oct 16, 2026·11 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI