Voice Waveform UI
Animated waveform bars with listening, recording and speaking states.
voicewaveformaudioindicator
Props
24
Component source
Single-file React + Tailwind component. No dependencies — drop it into any project with Tailwind configured.
voice-waveform.tsx
"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)}}`}</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="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>
</>
);
}
Usage Guidelines
- 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.
Common UX Mistakes
- 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
Voice Input
A voice input control that expands into a recording panel with waveform and transcript.
Voice Conversation
A voice conversation transcript with speaking indicators and per-turn replay.
Prompt Input
The core input for AI chat products — with attachments, model selector, tools, voice and loading states.
Prompt Input + Attachments
Prompt input with file attachment chips, upload progress and removal.