Browse all components
Voice Waveform UI
An animated audio waveform — distinct bar motion for listening, recording, and the assistant speaking back.
Preview
Presets
Props
Agent promptClaude Code · Cursor · any agent
Add the Voice Waveform UI component from the Scrim UI registry to this project and use it in the configuration described below.
Voice Waveform UI — An animated audio waveform — distinct bar motion for listening, recording, and the assistant speaking back.
## 1. Install
```bash
npx shadcn@latest add @scrimui/voice-waveform
```
This writes a single file to `components/ui/voice-waveform.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-waveform to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { VoiceWaveform } from "@/components/ui/voice-waveform";
<VoiceWaveform
state="idle"
bars={24}
/>
```
## 3. The configuration that matters
This is the component's "Idle" state — Flat and dim — nothing is happening, and the UI says so honestly.
These props were set deliberately and should be preserved as written:
- `state` = "idle" — State
Every other prop is at its default; leave those off the call site rather than writing the default value out.
Reference: https://scrimui.dev/components/voice-waveformFollows 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-waveformComponent source
React + Tailwind component using your shadcn theme. No additional component dependencies.
"use client";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type WaveformState = "idle" | "listening" | "recording" | "speaking";
export type VoiceWaveformProps = {
state?: WaveformState;
bars?: number;
className?: string;
};
/* Bars animate with the same keyframe; the state picks the speed,
opacity and color (color comes from the parent via currentColor). */
const DURATION: Record<WaveformState, number> = {
idle: 0,
listening: 1.1,
recording: 0.8,
speaking: 0.55,
};
const OPACITY: Record<WaveformState, number> = {
idle: 0.3,
listening: 0.7,
recording: 1,
speaking: 1,
};
/* ------------------------------------------------------------------ */
/* VoiceWaveform */
/* ------------------------------------------------------------------ */
export function VoiceWaveform({
state = "idle",
bars = 24,
className = "",
}: VoiceWaveformProps) {
const dur = DURATION[state];
const opacity = OPACITY[state];
return (
<>
<style>{`@keyframes aiui-wave{0%,100%{transform:scaleY(.25)}50%{transform:scaleY(1)}} @media(prefers-reduced-motion:reduce){.scrim-wave-bar{animation:none!important}}`}</style>
<div className={`flex h-8 items-center gap-[2px] ${className}`} aria-hidden>
{Array.from({ length: bars }).map((_, i) => {
/* static bell shape: center bars taller than the edges */
const t = i / Math.max(bars - 1, 1);
/* Rounded, not raw: a full-precision float lands in the SSR HTML as
scaleY(0.6943240406445356), and the browser hands it back to
hydration as scaleY(0.694324) — a mismatch React reports as an
error. Three decimals is well below a visible difference. */
const base = Number((0.3 + 0.7 * Math.sin(Math.PI * t)).toFixed(3));
return (
<span
key={i}
className="scrim-wave-bar w-[3px] rounded-full bg-current"
style={{
height: "100%",
transform: `scaleY(${base})`,
transformOrigin: "center",
animation: dur
? `aiui-wave ${dur}s ease-in-out ${i * (dur / bars)}s infinite`
: "none",
opacity,
}}
/>
);
})}
</div>
</>
);
}
When to use it
- Animate bars only when audio is actually flowing — a moving idle waveform is noise.
- Use distinct colors per state (green listening, red recording) so the state reads at a glance.
- Match the animation speed to real energy: faster bars for speaking, slower for listening.
- Keep the component silent-friendly — it should also work with a screen reader label.
What breaks in production
- Fake-animating a waveform while nothing is happening — users learn to distrust it.
- One ambiguous color for every state; states must be distinguishable without reading.
- Bars so short they read as static dots, or so tall they crowd the layout.
Related Components
A microphone button that expands into a recording panel — live waveform, a running transcript, and cancel or send.
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.