Browse all components
Voice Input UI
A microphone button that expands into a recording panel — live waveform, a running transcript, and cancel or send.
Preview
Presets
Props
Also takes onStart, onStop, onCancel.
Agent promptClaude Code · Cursor · any agent
Add the Voice Input UI component from the Scrim UI registry to this project and use it in the configuration described below.
Voice Input UI — A microphone button that expands into a recording panel — live waveform, a running transcript, and cancel or send.
## 1. Install
```bash
npx shadcn@latest add @scrimui/voice-input
```
This writes a single file to `components/ui/voice-input.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/voice-input to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { VoiceInput } from "@/components/ui/voice-input";
<VoiceInput
state="idle"
recordingTime="0:07"
transcript="Show me the streaming message component with a tool call in the middle"
onStart={() => {}}
onStop={() => {}}
onCancel={() => {}}
/>
```
## 3. The configuration that matters
This is the component's "Idle" state — A single microphone button; pressing it expands the control into recording.
Every prop is at its default value. Keep the call site minimal — do not write out default values.
## 4. Wire the handlers
`onStart`, `onStop`, `onCancel` are stubs in the snippet above (`() => {}`). Connect them to this project's own state and data layer instead of shipping the empty functions.
Reference: https://scrimui.dev/components/voice-inputFollows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.
Install
npx shadcn@latest add @scrimui/voice-inputComponent source
React + Tailwind component using shadcn/ui (button, card). The install command adds these dependencies automatically.
When copying source manually, install the required primitives first:
npx shadcn@latest add button card"use client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type VoiceInputState = "idle" | "recording";
export type VoiceInputProps = {
state?: VoiceInputState;
onStart?: () => void;
onStop?: () => void;
onCancel?: () => void;
recordingTime?: string;
transcript?: string;
className?: string;
};
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
function MicIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
width="16"
height="16"
{...props}
>
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
<path d="M12 19v3" />
</svg>
);
}
function StopIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" width="11" height="11" {...props}>
<rect x="6" y="6" width="12" height="12" rx="2" />
</svg>
);
}
function XIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
width="12"
height="12"
{...props}
>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* Bars */
/* ------------------------------------------------------------------ */
function Bars({ active }: { active: boolean }) {
return (
<>
<style>{`@keyframes aiui-bar{0%,100%{transform:scaleY(.35)}50%{transform:scaleY(1)}}`}</style>
<div className="flex h-5 items-center gap-[3px]" aria-hidden>
{Array.from({ length: 9 }).map((_, i) => (
<span
key={i}
className="w-[3px] rounded-full bg-current"
style={{
height: "100%",
transform: "scaleY(.35)",
transformOrigin: "center",
animation: active ? `aiui-bar 0.8s ease-in-out ${i * 0.06}s infinite` : "none",
opacity: active ? 1 : 0.3,
}}
/>
))}
</div>
</>
);
}
/* ------------------------------------------------------------------ */
/* VoiceInput */
/* ------------------------------------------------------------------ */
export function VoiceInput({
state = "idle",
onStart,
onStop,
onCancel,
recordingTime = "0:07",
transcript = "",
className = "",
}: VoiceInputProps) {
if (state === "recording") {
return (
<Card
className={`gap-0 py-0 rounded-xl border border-red-200 bg-card p-3 dark:border-red-900/50 ${className}`}
>
<div className="flex items-center gap-3">
<Button variant="destructive" size="icon-sm"
type="button"
onClick={onStop}
aria-label="Stop recording"
className="min-h-6 min-w-6 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-red-500 text-white transition-transform"
>
<StopIcon />
</Button>
<div className="min-w-0 flex-1 text-red-500 dark:text-red-400">
<Bars active />
</div>
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">{recordingTime}</span>
{onCancel && (
<Button variant="ghost" size="icon-sm"
type="button"
onClick={onCancel}
aria-label="Cancel recording"
className="min-h-6 min-w-6 shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-muted-foreground"
>
<XIcon />
</Button>
)}
</div>
<p className="mt-2 text-sm text-muted-foreground">
{transcript || "Listening…"}
</p>
</Card>
);
}
return (
<Card
className={`flex-row gap-0 py-0 flex items-center gap-3 rounded-xl border border-border bg-card p-3 ${className}`}
>
<Button variant="default" size="icon-sm"
type="button"
onClick={onStart}
aria-label="Start voice input"
className="min-h-6 min-w-6 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-transform"
>
<MicIcon />
</Button>
<span className="text-sm text-muted-foreground">Click to talk</span>
<div className="ml-auto text-muted-foreground">
<Bars active={false} />
</div>
</Card>
);
}
When to use it
- Offer voice next to text, never instead of it — some users always prefer typing.
- Make the recording state unmistakable: red accent, live waveform, elapsed time.
- Show a live transcript as speech is captured so users can catch mis-transcriptions early.
- Provide Stop (finish) and Cancel (discard) as separate, clearly labeled actions.
What breaks in production
- A microphone icon with no hint of what happens when you click it.
- Starting to record with no visible change — the user is unsure whether they are being recorded.
- No way to discard a recording; users stay stuck with a half-spoken message.
Guides
Related Components
An animated audio waveform — distinct bar motion for listening, recording, and the assistant speaking back.
A voice conversation as it happens — who is speaking, the live transcript, and replay for any individual turn.
The chrome around a live voice call — an unmistakable mute toggle, a running call timer, and an end button that's always one tap away.
The message input at the heart of an AI chat app — file attachments, model picker, tool toggles, voice, and a send button that turns into stop.