# 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.

- Type: React component (AI Kit)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add voice-input`
- Docs: https://wingo-ui.com/components/voice-input

## Interaction

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.

## Installation

```bash
npx wingo-ui@latest add voice-input
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/voice-input` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/voice-input.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Matrix Orb](https://wingo-ui.com/components/matrix-orb) ([Markdown](https://wingo-ui.com/components/matrix-orb.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
"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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant?` | `"button" \| "bar" \| "orb"` | `"button"` | 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. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | 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. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | 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. |
| `color?` | `string` | `""` | Any 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. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"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. |
| `bars?` | `number` | `0` | How many waveform bars. 0 fits the width with 3px bars and 2px gaps; a fixed count spreads the bars across the pill. |
| `transcript?` | `string` | `""` | Words 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. |
| `showTranscript?` | `boolean` | `true` | Shows 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. |
| `showWaveform?` | `boolean` | `true` | Shows 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. |
| `showTimer?` | `boolean` | `true` | Shows the elapsed time (0:07). It turns amber and pulses in the last 10 seconds before maxDuration. |
| `defaultStatus?` | `"idle" \| "requesting" \| "recording" \| "processing" \| "error"` | `"idle"` | 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) => void` |  | Fires on every change: requesting after the tap, recording, processing after Send, idle or error. |
| `error?` | `"denied" \| "noMic" \| "unsupported" \| "tooShort" \| "interrupted" \| "failed" \| string` | `""` | What 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. |
| `speaking?` | `boolean` | `false` | Orb 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. |
| `disabled?` | `boolean` | `false` | Dims everything and blocks starting; a recording in progress is canceled. |
| `mode?` | `"toggle" \| "hold"` | `"toggle"` | 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. |
| `audio?` | `boolean` | `true` | Records 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. |
| `transcribe?` | `"browser" \| "none"` | `"browser"` | 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. |
| `lang?` | `string` | `""` | The recognition language, like "en-US". Empty uses the resolved locale. |
| `maxDuration?` | `number` | `120` | Seconds. The timer warns 10 seconds before, and at the limit the recording stops, sends itself and announces labels.maxReached. 0 means no limit. |
| `minDuration?` | `number` | `500` | Milliseconds. A shorter recording is a mis-tap: it is discarded and labels.tooShort shows for a moment. |
| `haptics?` | `boolean` | `true` | A 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. |
| `locale?` | `string` | `""` | The locale that picks the recognition language when lang is empty. Empty uses the UIProvider's, then the page's. |
| `getLevel?` | `() => number` |  | A 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 }) => void` |  | Live 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?` | `() => void` |  | Cancel, Escape or a hold released while armed. The recording is gone. |
| `labels?` | `Partial<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}`. |
| `classNames?` | `Partial<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. |
| `className?` | `string` |  | Extra classes for the root, merged last. The root sets the --tone vars, so [--tone:purple] recolors the waveform and Send. |

## Parts

### VoiceWaveform (component)

The 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 (hook)

The 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 (constant)

The 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.

## See also

- [Prompt Input](https://wingo-ui.com/components/prompt-input) ([Markdown](https://wingo-ui.com/components/prompt-input.md))
- [Matrix Orb](https://wingo-ui.com/components/matrix-orb) ([Markdown](https://wingo-ui.com/components/matrix-orb.md))
- [Fluid Orb](https://wingo-ui.com/components/fluid-orb) ([Markdown](https://wingo-ui.com/components/fluid-orb.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))

---

Canonical: https://wingo-ui.com/components/voice-input
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
