Skip to main content
Clever Ops

Voice Waveform Bar

Voice agent waveform bar for React, prop-driven amplitude bars across idle, listening, processing, and speaking states, with Tailwind token styling and zero microphone access.

Preview & code

Loading Voice Waveform Bar preview

Installation

pnpm dlx shadcn@latest add @cleverui/voice-waveform-bar

Requires the @cleverui registry in your components.json, a one-off, two-line setup.

Usage

tsx
import { VoiceWaveformBar } from '@/components/ui/voice-waveform-bar'
Exampletsx
'use client'

import { useEffect, useMemo, useState } from 'react'
import { useReducedMotion } from 'motion/react'
import {
  VoiceWaveformBar,
  type VoiceWaveformState,
} from '@/components/ui/voice-waveform-bar'

const BAR_COUNT = 24

/** Deterministic sine-derived amplitude frames, no Math.random in render */
function buildSineFrame(phase: number, amplitude: number): number[] {
  return Array.from({ length: BAR_COUNT }, (_, i) => {
    const a =
      amplitude *
      (0.35 +
        0.65 *
          Math.abs(
            Math.sin(phase + i * 0.42) * 0.55 +
              Math.sin(phase * 1.7 + i * 0.18) * 0.45
          ))
    return Math.max(0.06, Math.min(1, a))
  })
}

// precomputed sequence stepped by index only
const LISTEN_FRAMES = Array.from({ length: 32 }, (_, i) =>
  buildSineFrame(i * 0.35, 0.85)
)
const SPEAK_FRAMES = Array.from({ length: 32 }, (_, i) =>
  buildSineFrame(i * 0.28 + 1.2, 0.7)
)

type Phase = {
  state: VoiceWaveformState
  duration: string
  transcript: string
  holdMs: number
  levels?: number[]
}

const PHASES: Phase[] = [
  {
    state: 'idle',
    duration: '0:00',
    transcript: 'Tap the mic to start a voice turn with Meridian.',
    holdMs: 1600,
  },
  {
    state: 'listening',
    duration: '0:04',
    transcript: 'Reconcile May invoices for Apex Holdings…',
    holdMs: 2200,
  },
  {
    state: 'listening',
    duration: '0:07',
    transcript: 'Focus Acme Logistics and flag anything over fifty dollars.',
    holdMs: 2000,
  },
  {
    state: 'processing',
    duration: '0:09',
    transcript: 'Matching payments against the Xero trial balance…',
    holdMs: 1800,
  },
  {
    state: 'speaking',
    duration: '0:14',
    transcript:
      'Found a duplicate $1,240 charge on invoice inv_2026_03_8841.',
    holdMs: 2400,
  },
  {
    state: 'speaking',
    duration: '0:18',
    transcript: 'Shall I draft the refund proposal for controller review?',
    holdMs: 2200,
  },
  {
    state: 'idle',
    duration: '0:18',
    transcript: 'Turn complete, awaiting your next instruction.',
    holdMs: 2000,
  },
]

export default function VoiceWaveformBarDemo() {
  const reduced = useReducedMotion()
  const [phaseIndex, setPhaseIndex] = useState(0)
  const [frameTick, setFrameTick] = useState(0)
  const [listening, setListening] = useState(false)

  const phase = PHASES[phaseIndex] ?? PHASES[0]!

  useEffect(() => {
    if (reduced) {
      setPhaseIndex(4) // speaking, composed mid-session frame
      return
    }
    const id = window.setTimeout(() => {
      setPhaseIndex((i) => (i + 1) % PHASES.length)
    }, phase.holdMs)
    return () => window.clearTimeout(id)
  }, [phaseIndex, phase.holdMs, reduced])

  // step amplitude frames while listening/speaking
  useEffect(() => {
    if (reduced) return
    if (phase.state !== 'listening' && phase.state !== 'speaking') return
    const id = window.setInterval(() => {
      setFrameTick((t) => t + 1)
    }, 80)
    return () => window.clearInterval(id)
  }, [phase.state, reduced])

  const levels = useMemo(() => {
    if (phase.state === 'listening') {
      return LISTEN_FRAMES[frameTick % LISTEN_FRAMES.length]
    }
    if (phase.state === 'speaking') {
      return SPEAK_FRAMES[frameTick % SPEAK_FRAMES.length]
    }
    return undefined
  }, [phase.state, frameTick])

  const state: VoiceWaveformState = listening
    ? 'listening'
    : reduced
      ? 'speaking'
      : phase.state

  const duration = reduced ? '0:14' : phase.duration
  const transcript = reduced
    ? 'Found a duplicate $1,240 charge on invoice inv_2026_03_8841.'
    : listening
      ? 'Listening, speak naturally, Meridian is capturing…'
      : phase.transcript

  return (
    <div className="mx-auto flex min-h-[320px] w-full max-w-md flex-col justify-center px-4 py-16 sm:px-6">
      <div className="overflow-hidden rounded-2xl border border-border/60 bg-card">
        <div className="border-b border-border/60 px-4 py-3 sm:px-5">
          <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground sm:text-xs">
            meridian-ops · voice turn
          </p>
        </div>
        <div className="px-4 py-6 sm:px-5">
          <VoiceWaveformBar
            state={state}
            levels={
              listening
                ? LISTEN_FRAMES[frameTick % LISTEN_FRAMES.length]
                : levels
            }
            duration={duration}
            transcript={transcript}
            onToggle={() => setListening((v) => !v)}
            label="voice"
          />
        </div>
      </div>
    </div>
  )
}

API reference

VoiceWaveformBar accepts the following props.

PropTypeDefaultDescription
stateVoiceWaveformState'idle'voice loop phase, fully prop-driven
levelsnumber[]amplitude frame as 0–1 samples (~24); omitted → flat baseline
durationstringpreformatted elapsed readout, e.g. '0:42'
transcriptstringlatest caption line under the pill
onToggle() => voidmic button callback; omitted hides the button
labelstring'voice'mono micro-label opening the instrument

…plus everything from Omit< React.HTMLAttributes<HTMLDivElement>, 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' >: className, event handlers, aria attributes and the rest pass straight through.

Referenced types

Typestsx
export type VoiceWaveformState =
  | 'idle'
  | 'listening'
  | 'processing'
  | 'speaking'

Frequently asked questions

How do I install the Voice Waveform Bar component?

Register the @cleverui registry in your components.json ("@cleverui": "https://cleverops.com.au/r/{name}.json"), then run `npx shadcn@latest add @cleverui/voice-waveform-bar`. The shadcn CLI copies the source into your project, you own the code from that point. You can also copy the source directly from this page.

What dependencies does Voice Waveform Bar require?

Voice Waveform Bar uses motion and lucide-react on top of React and Tailwind CSS. You don't need to install them manually, the shadcn CLI resolves and installs npm dependencies automatically when you add the component.

Can I theme Voice Waveform Bar to match my brand?

Yes. The component is styled entirely with Tailwind utilities over standard design tokens (primary, background, muted, border), so it inherits your existing shadcn/ui theme automatically. Change your CSS variables and the component follows, no source edits needed for palette, radius, or fonts.

Want this built for you?

CleverUI is built by the Clever Ops web team. If you'd rather ship a hand-coded, fast, custom website than assemble one, we build them for Australian businesses every week.

Explore web development services