Voice Input
Press the mic and say the change. Escape cancels, the tick finishes.
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/voice-input.jsonUsage
import { VoiceInput } from "@/components/ui/voice-input"
<VoiceInput onStop={async ({ audio }) => setText(await transcribe(audio))} />Examples
Push to talk
mode="hold" records while the button or Space is held. A tap under 300ms is treated as a slip and says to hold. maxDuration stops it at 30s and turns the timer amber for the last ten.
States
Driven by the state prop, with a simulated level and no microphone.
With your own speech engine
microphone={false} leaves capture to your SDK: pass its input level as level and show its interim words yourself.
API reference
Renders a div holding the button, the tape, the timer and Cancel. Other div attributes go on it.
| Prop | Type | Default |
|---|---|---|
mode | "toggle"toggle: press to start and again to finish. hold: talk while the button or Space is held. | "toggle" |
microphone | booleanAsk for the microphone on start, drive the tape from it and record it with MediaRecorder. Nothing is requested until the first press. | true |
level | numberInput level, 0 to 1. Overrides the microphone's reading. | None |
onStart | () => voidCalled on press, before any permission prompt. | None |
onStop | (recording: { audio?: Blob; duration: number }) => voidCalled when the speaker finishes. Return a promise to show Transcribing until it settles; a rejection shows an error. | None |
onCancel | () => voidCalled on Cancel or Escape. The recording is discarded and a pending transcription is ignored. | None |
onError | (error: unknown) => voidThe microphone was refused, missing or failed, or the stop promise rejected. | None |
state | VoiceInputStateControlled state: idle, requesting, listening, transcribing, done, denied or error. | None |
defaultState | VoiceInputStateInitial state when uncontrolled. | "idle" |
onStateChange | (state: VoiceInputState) => voidCalled on every transition. | None |
maxDuration | numberSeconds. Recording finishes by itself at this length. | None |
size | "sm"sm is a 32px toolbar control that grows into a strip; md is a full-width bar. | "sm" |
label | stringAccessible name of the button at rest. | "Dictate" |
hint | stringmd only: the line on the display at rest. | None |
disabled | booleanDisables the button. | false |
color | NeonColorPalette name or hex. | inherited |
Accessibility
- The microphone is only requested from a press, never during render or on mount, so server rendering and hydration never touch it.
- One button keeps focus through every state; its name changes from Dictate to Finish and transcribe, and a status region announces Listening, Transcribing, Done or the error.
- Escape cancels while listening, waiting for permission or transcribing.
- In hold mode Space and Enter work like the pointer, aria-pressed reports the hold, and releasing during the permission prompt cancels quietly.
- A refused microphone shows Mic blocked next to the button and describes it with how to fix it; pressing again retries.
- Under reduced motion the scrolling tape becomes a still LED level that updates four times a second, and the readback playhead becomes the word Transcribing.