Voice Composer
Switches between a controlled text composer and browser-backed voice capture with a real-time waveform.
Try me!
Voice Composer is a controlled message form with two views: a text composer and a live voice recorder. It owns microphone capture, browser capability routing, the recording lifecycle, and transcript insertion. Your application owns the message draft, submission, and the fallback transcription service.
The component reuses LineWaveform for both real microphone input and the
processing state. Change waveformMode to choose a centered static spectrum or
a waveform that scrolls from right to left.
Browser strategy
Chrome and Edge use the Web Speech API when it is available. Recognition runs
continuously and may produce interim results, but Voice Composer only keeps
final results and does not write them into the draft until the user selects
Stop. LineWaveform still acquires the microphone stream in this path so the
voice view shows real audio activity.
Other browsers use MediaRecorder with the same stream owned by
LineWaveform. The component probes WebM/Opus, Ogg/Opus, then MP4 and passes
the completed Blob to onAudioRecorded. Return the final transcript from
that callback. When this callback is absent, the Voice action is unavailable
on browsers that need the fallback.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Provides the controlled message draft. |
onValueChange | (value: string) => void | — | Receives text edits and the draft with a completed transcript inserted. |
onSubmit | (value: string, event: React.FormEvent<HTMLFormElement>) => void | Promise<void> | — | Receives the current draft and form event when the text composer submits. |
onAudioRecorded | (audio: Blob) => Promise<string> | undefined | Transcribes the MediaRecorder fallback Blob and resolves with final text. |
onTranscriptionChange | (text: string) => void | undefined | Receives the completed transcript before it is inserted into the draft. |
onError | (error: { code: VoiceComposerErrorCode; message: string; cause?: unknown }) => void | undefined | Receives a structured capture, recognition, transcription, or empty-result error. |
lang | string | "en-US" | Sets the language passed to browser speech recognition. |
disabled | boolean | false | Disables composing, voice capture, and submission. |
maxDurationSeconds | number | 120 | Sets the recording limit in seconds. Reaching it stops and transcribes the recording. |
waveformMode | "static" | "scrolling" | "scrolling" | Forwards the visual mode to LineWaveform. |
VoiceComposer also accepts standard form props except onSubmit and
onError, which use the contracts above.
VoiceComposerErrorCode
type VoiceComposerErrorCode =
| "unsupported"
| "permission_denied"
| "capture_failed"
| "recognition_failed"
| "transcription_failed"
| "empty_transcript"Behavior
Selecting Voice saves the textarea selection and starts microphone setup while the text composer remains visible. The composer moves to the voice view only after recording has actually started, so permission setup is not exposed as a third visual mode. Stop completes recognition or recording, then inserts the final transcript once at the saved selection and restores textarea focus. Cancel discards the current voice result without changing the draft.
Recording automatically stops after maxDurationSeconds. The transcription
stage cannot be cancelled because the component cannot abort an arbitrary
onAudioRecorded promise. All errors return to the text view, preserve the
draft, and call onError; the component does not render an error message or
toast.
The text and voice views move horizontally, and the inactive view becomes
inert immediately so its controls cannot be activated during the transition.
Transcription stays in the voice view and uses its processing waveform and
status text instead of adding another action state. Motion respects the user's
reduced-motion preference. A persistent polite live region announces capture
and transcription status.