Voice Visualizer

The agent-presence indicator for a voice UI — dots (or bars) that move with the agent's state and audio level. It's the “is this thing on?” signal, so it never goes dead.

Cycle through connecting → listening → thinking → speaking.

tsx
"use client"

import * as React from "react"

import { Button } from "@/registry/seam/ui/button"
import { VoiceVisualizer } from "@/registry/seam/ui/voice-visualizer"

const STATES = ["connecting", "listening", "thinking", "speaking"] as const

export default function VoiceVisualizerDemo() {
  const [i, setI] = React.useState(1)
  const state = STATES[i]
  // "listening"/"speaking" react to a live level; fake one when there's no mic.
  const [level, setLevel] = React.useState(0)

  React.useEffect(() => {
    if (state !== "listening" && state !== "speaking") return
    const id = setInterval(() => setLevel(0.2 + Math.abs(Math.sin(performance.now() / 300)) * 0.8), 80)
    return () => clearInterval(id)
  }, [state])

  return (
    <div className="flex flex-col items-center gap-5">
      <VoiceVisualizer state={state} level={level} size="lg" />
      <Button
        variant="secondary"
        size="sm"
        onClick={() => setI((n) => (n + 1) % STATES.length)}
      >
        State: {state}
      </Button>
    </div>
  )
}
bunx --bun seamui@latest add voice-visualizer

API

PropTypeDefaultDescription
state"disconnected" | "connecting" | "listening" | "thinking" | "speaking""listening"Drives the motion and the accessible label.
levelnumberAudio level 0–1; takes precedence over track analysis.
trackMediaStreamTrack | nullAnalysed via Web Audio by the owned useAudioLevel hook.
countnumber5Number of dots or bars.
size"sm" | "default" | "lg""default"Dot/bar dimensions.
variant"dots" | "bars""dots"Dots scale uniformly; bars stretch vertically.

Notes

  • Each state has its own motion: connecting is a staggered opacity shimmer, listening breathes with the mic level (spring-smoothed on springs.snappy so raw analyser values never jitter the UI), thinking is a sequential opacity sweep, and speaking scales with the agent's output level, weighted so the center moves most.
  • Every channel maps to opacity under reduced motion — a call UI's liveness signal must never freeze.
  • Transport-agnostic: pass a numeric level or a track for the owned useAudioLevel hook. The prop shape maps 1:1 onto the AI SDK / LiveKit useVoiceAssistant hooks with no runtime dependency.
  • A role="status" labelled from the state (“Agent is listening”); the dots are aria-hidden, and VoiceVisualizerCaption is the visible equivalent of that label.

Press feedback, reduced motion, and haptics follow the global policy — see Motion and Haptics.