Voice Input React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
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.
Tap the mic and, once the browser has your permission (asked only after that first tap; the mic spins meanwhile and a desktop tooltip says to allow it), it grows on a smooth spring into a pill: Cancel, a pulsing red dot, a waveform that ripples with your voice, a timer and Send, and it shrinks back into the mic when you are done. The words you say appear above it as the browser hears them, the unsure ones muted; Send hands the app the audio and the transcript, Cancel or Escape throws it away. Ten seconds before maxDuration the timer turns amber and pulses, and at the limit the recording sends itself. In hold mode you press and hold to talk and let go to send; slide more than 80px toward the start, or off the pill, and it tints red with a tick of haptics, so letting go cancels instead. A tap that is too short says so and fades, with the mic still there for the next try. A blocked or missing microphone explains itself in place with Try again, and a send your promise rejects can be sent again. The bar variant is always there at full width, and a tap anywhere on it starts; the orb variant is a voice mode, listening, thinking while your promise runs and blooming with getLevel while the app talks back. With a keyboard, Tab to the mic and press Enter or Space; focus moves to Send while recording, Escape cancels (or closes an error), and in hold mode holding Space records and releasing it sends while Enter works like a tap. Screen readers hear Listening, Transcribing and the recorded length once, never the ticking timer. On phones every target is 44px, the held mic stops the page from scrolling, and a halo under your finger breathes with your voice. Reduced motion swaps the morph for a cross-fade and the waveform for a still level meter.
Voice Input is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add voice-input, together with Button, Matrix Orb, Spinner, Tooltip, useControllableState and 7 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add voice-inputVoice Input is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import VoiceInput from @/components/ui/voice-input.
"use client"
import { useState } from "react"
import { VoiceInput, type VoiceInputResult } from "@/components/ui/voice-input"
export function VoiceNote({ clientId }: { clientId: string }) {
const [note, setNote] = useState("")
async function save(result: VoiceInputResult) {
if (result.transcript) setNote((text) => [text, result.transcript].filter(Boolean).join(" "))
if (!result.blob) return
const body = new FormData()
body.append("audio", result.blob, "voice-note.webm")
const response = await fetch(`/api/clients/${clientId}/voice-notes`, { method: "POST", body })
if (!response.ok) throw new Error("Upload failed")
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm">{note || "No note yet"}</p>
<VoiceInput
variant="bar"
lang="en-US"
maxDuration={180}
onRecordingComplete={save}
/>
</div>
)
}Props
variantdefault"button""button" | "bar" | "orb"button: a mic icon button that expands in place into the recording pill, for composers and toolbars. bar: the full-width bar, always visible like WhatsApp's, with the transcript above it. orb: a MatrixOrb voice mode that listens, thinks and blooms while the app talks back.
sizedefault"md""sm" | "md" | "lg"The mic is the button's icon-sm / icon / icon-lg (32 / 40 / 48px, 44px on touch for md) and the pill keeps its height, the bar is 40 / 48 / 56px tall, the orb 160 / 220 / 280px wide.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Colors the waveform, the Send button, the bar's mic and the orb. The recording dot stays red whatever the tone, because red means recording.
colordefault""stringAny CSS color. Replaces the tone for the waveform, the buttons and the orb (the orb's canvas gets the resolved color, so oklch and CSS variables work too). Leave empty to use the tone.
radiusdefault"full""none" | "sm" | "md" | "lg" | "full"Corners of the mic, the pill and the bar. The buttons inside the pill take its corner minus their 4px inset, so every size reads as one shape.
barsdefault0numberHow many waveform bars. 0 fits the width with 3px bars and 2px gaps; a fixed count spreads the bars across the pill.
transcriptdefault""stringWords the app streams in while recording (a server transcriber) or while speaking (the answer in voice mode). Wins over the browser's recognizer and becomes the result's transcript.
showTranscriptdefaulttruebooleanShows the live transcript: above the pill and the bar (four lines, scrolled to the newest words), under the orb in large centered type. Final words in the text color, interim ones muted.
showWaveformdefaulttruebooleanShows the live waveform in the pill. Hidden, the pill says Recording in its place. In hold mode the slide hint takes this spot and the level shows as a halo around the held mic.
showTimerdefaulttruebooleanShows the elapsed time (0:07). It turns amber and pulses in the last 10 seconds before maxDuration.
defaultStatusdefault"idle""idle" | "requesting" | "recording" | "processing" | "error"The starting status. recording without a tap never opens the microphone: the timer runs, getLevel drives the visuals and Send hands over the length and the transcript prop, which is how demos and screenshots show the pill.
status"idle" | "requesting" | "recording" | "processing" | "error"Controlled status, with onStatusChange. Drive processing yourself while a server transcribes, or set idle to stop listening from outside (the recording is discarded).
onStatusChange(status: VoiceInputStatus) => voidFires on every change: requesting after the tap, recording, processing after Send, idle or error.
errordefault"""denied" | "noMic" | "unsupported" | "tooShort" | "interrupted" | "failed" | stringWhat status error shows. A kind picks its sentence from labels (denied, noMic, unsupported, tooShort, interrupted, failed); any other text shows as it is, like a message from your server. Empty shows the component's own error.
speakingdefaultfalsebooleanOrb variant: the app is talking back. The orb blooms with getLevel (your text to speech output), the status reads Speaking and the transcript prop shows the answer. A tap on the mic interrupts it.
disableddefaultfalsebooleanDims everything and blocks starting; a recording in progress is canceled.
modedefault"toggle""toggle" | "hold"toggle: tap to start, then Send or Cancel. hold: push to talk, release to send, slide away (or Escape) to cancel; the held element sets touch-action none and keeps the pointer while it turns into the pill, and Space works the same way from the keyboard.
audiodefaulttruebooleanRecords audio with MediaRecorder (audio/webm with Opus, audio/mp4 on Safari) and hands the Blob to onRecordingComplete. Off, only the transcript and the length are sent.
transcribedefault"browser""browser" | "none"browser: live words from the Web Speech API where it exists (Chrome, Edge, Safari), restarted when Chrome ends a run; elsewhere the transcript stays empty and the audio still records. none: no live words, for server transcription.
langdefault""stringThe recognition language, like "en-US". Empty uses the resolved locale.
maxDurationdefault120numberSeconds. The timer warns 10 seconds before, and at the limit the recording stops, sends itself and announces labels.maxReached. 0 means no limit.
minDurationdefault500numberMilliseconds. A shorter recording is a mis-tap: it is discarded and labels.tooShort shows for a moment.
hapticsdefaulttruebooleanA light tick on start, success on send, warning on cancel and a medium tick when a slide arms cancel. Touch and pen only; a mouse never vibrates a laptop.
localedefault""stringThe locale that picks the recognition language when lang is empty. Empty uses the UIProvider's, then the page's.
getLevel() => numberA 0 to 1 level read every frame in place of the microphone analyser: a WebRTC call, your text to speech output in voice mode, or a simulation. With getLevel, audio off and transcribe none, nothing opens the microphone at all.
onRecordingComplete(result: VoiceInputResult) => (void | Promise<unknown>)Send, the hold release or the time limit: { blob?, mimeType?, durationMs, transcript? }. Return a promise to keep the processing spinner until it settles; a rejection shows the failed error and Try again sends the same recording again.
onTranscript(text: string, info: { final: boolean }) => voidLive words from the browser's recognizer: the whole transcript of this recording so far, final once no words are pending. Dictation into a field uses it.
onCancel() => voidCancel, Escape or a hold released while armed. The recording is gone.
labelsPartial<VoiceInputLabels>Every string, English by default (DEFAULT_VOICE_INPUT_LABELS): start, stop, send, cancel, recording, listening, speaking, requesting, processing, denied, unsupported, noMic, tooShort, interrupted, failed, holdHint, tapHint, releaseToSend, slideToCancel, releaseToCancel, retry, dismiss, maxReached and recorded(duration), e.g. (d) => `Recording, ${d}`.
classNamesPartial<Record<"trigger" | "bar" | "cancel" | "dot" | "waveform" | "timer" | "send" | "hint" | "transcript" | "orb" | "status" | "controls" | "error" | "retry", string>>Extra classes for the parts: the mic, the pill or bar, Cancel, the recording dot, the waveform, the timer, Send, the hint, the transcript, the orb, its status line and controls row, the error line and Try again.
classNamestringExtra classes for the root, merged last. The root sets the --tone vars, so [--tone:purple] recolors the waveform and Send.
Parts
VoiceWaveform
componentThe bars on their own, fed by any level source: level (a MotionValue), getLevel, or values for the still waveform of a recorded note; bars, active, idleHeight, tone, color. Transforms are written straight to the bars in a rAF loop, never through React state, and pause off screen. Props: VoiceWaveformProps.
useVoiceRecorder
hookThe engine for custom UIs: { status, start, stop, cancel, retry, dismiss, level, elapsed, transcript, interim, error, supported, metered }. Options mirror the component (status, audio, transcribe, lang, getLevel, minDuration, maxDuration, onComplete, onTranscript, onCancel, onError, onLimit). start only works from a tap or a key press; every track, context and recognizer stops on stop, cancel and unmount, and a second recorder starting stops the first. Signature: (options: UseVoiceRecorderOptions) => VoiceRecorder.
DEFAULT_VOICE_INPUT_LABELS
constantThe English strings, exported so an app can translate from a complete object. Type: VoiceInputLabels.
Examples
Bar
The full-width bar under a note field; the live transcript sits between them and lands in the note.
Voice mode
The orb in a dark card: it listens, thinks while the promise runs, then blooms while the answer is spoken.
Push to talk
mode="hold" like WhatsApp: release to send, slide away to cancel, with a halo under the finger.
Server transcription
transcribe="none" and onRecordingComplete returning a promise: Transcribing… until the text comes back.
Errors
Blocked microphone with a retry, a browser without voice input, and a tap that was too short.
Tones and sizes
sm, md and lg mics, a brand bar already recording and a small info bar.
Dependencies
Files
- components/ui/voice-input.tsx
FAQ
Is the React Voice Input component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Voice Input?
Run npx wingo-ui@latest add voice-input. It also adds the Wingo UI items it builds on (Button, Matrix Orb, Spinner, Tooltip, useControllableState and 7 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/voice-input.
How does Voice Input behave on phones?
On phones every target is 44px, the held mic stops the page from scrolling, and a halo under your finger breathes with your voice.
See also
Used in
More React AI components
Articles using this component
- React Audio Player with Waveform: Scrub, Seek, Peaks
Component guides9 min read
- React Prompt Input: Enter, IME, Files, Stop and Voice
Building UI with AI agents10 min read
- React Speech to Text Input: Mic, Waveform, Fallback
Building UI with AI agents9 min read