51 / Agent UI
Bar Visualizer
Frequency bars for audio on its way out, the counterpart to the trace a microphone draws on the way in.
"use client"
import * as React from "react"
import { Pause, Play } from "lucide-react"
import { SAMPLE_AUDIO } from "@/components/demos/audio-fixtures"
import { BarVisualizer } from "mischief-ui/bar-visualizer"
export function BarVisualizerDemo() {
const audioRef = React.useRef<HTMLAudioElement>(null)
const [element, setElement] = React.useState<HTMLAudioElement | null>(null)
const [speaking, setSpeaking] = React.useState(false)
React.useEffect(() => setElement(audioRef.current), [])
return (
<div className="border-border bg-background flex w-full max-w-md items-center gap-3 rounded-xl border p-3">
<audio
ref={audioRef}
src={SAMPLE_AUDIO}
preload="metadata"
onPlay={() => setSpeaking(true)}
onPause={() => setSpeaking(false)}
onEnded={() => setSpeaking(false)}
/>
<button
type="button"
aria-label={speaking ? "Stop the reply" : "Play the reply"}
onClick={() => {
const audio = audioRef.current
if (!audio) return
if (speaking) audio.pause()
else void audio.play().catch(() => {})
}}
className="bg-foreground text-background focus-visible:ring-ring inline-flex size-10 shrink-0 items-center justify-center rounded-full focus-visible:ring-2 focus-visible:outline-none"
>
{speaking ? (
<Pause aria-hidden="true" size={16} />
) : (
<Play aria-hidden="true" size={16} />
)}
</button>
<BarVisualizer
source={element}
state={speaking ? "speaking" : "idle"}
className="h-10 flex-1"
/>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/bar-visualizerimport { BarVisualizer } from "mischief-ui/bar-visualizer"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react" import { RenderSurface, useThemeColors, type SurfaceColor,} from "@/registry/default/render-surface/render-surface"import { cn } from "@/lib/utils" export type BarVisualizerState = "idle" | "listening" | "speaking" export type BarVisualizerSource =Usage
export function Speaking({ audio }) {
return <BarVisualizer source={audio} state="speaking" />
}What it can be given
A MediaStream for audio arriving, an audio or video element for audio playing, or an AnalyserNode you already built and would rather keep control of.
| Source | When |
|---|---|
MediaStream | A microphone, or a track from a call |
HTMLMediaElement | A reply being played back |
AnalyserNode | You already have a graph and want one tap |
An element can be handed to createMediaElementSource exactly once, and a second attempt throws for the rest of the page's life. Development remounts every effect twice, so that tap is kept and reused rather than rebuilt, and the element goes back to the speakers when this component unmounts.
It says the state as well as drawing it
Bars that move are the whole point of this, which makes it useless under prefers-reduced-motion: a single painted frame would show a bar chart frozen mid-sentence, saying nothing about whether anything is happening.
So state is a prop rather than something inferred from the levels. It carries the answer in words through a live region in both cases, and where motion is reduced the bars fall back to an even resting row instead of a misleading still.
API
sourceMediaStream | HTMLMediaElement | AnalyserNode | nullThe audio to draw. Without one the bars rest.state"idle" | "listening" | "speaking"What is happening, announced and put on the element.barsnumberHow many bars to draw. Defaults to 24.colorstringA theme token for the bars. Defaults to "--primary"....rootPropsHTMLAttributes<HTMLDivElement>Native root attributes.Accessibility
The state is announced through a polite live region in words, so a reader who cannot see the bars still learns that the assistant is listening or speaking. It is also on the element as data-state. Under prefers-reduced-motion the drawing is replaced by a still row of bars rather than a frozen frame, because a stopped visualiser reads as a stopped conversation.