Voice orb
FreeAn animated orb that shows idle, listening, thinking and speaking states.
pnpm dlx shadcn@latest add https://caliperui.site/r/voice-orb.jsonWhen to use #
Use it for
- A voice assistant needs a clear, calm status indicator.
- You want reduced-motion users to still see the state.
Skip it when
- The state can be shown with a short label alone.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/voice-orb.json
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
example.tsx
import { useEffect, useRef, useState } from "react" import { Button } from "@/components/ui/button" import { VoiceOrb, type VoiceState } from "@/components/ui/voice-orb" export function VoiceOrbDemo() { const [state, setState] = useState<VoiceState>("idle"); const [level, setLevel] = useState(0); const timers = useRef<ReturnType<typeof setTimeout>[]>([]); useEffect(() => () => timers.current.forEach(clearTimeout), []); useEffect(() => { if (state !== "listening" && state !== "speaking") return; let t = 0; const id = setInterval(() => { t += 0.2; setLevel(Math.max(0, Math.min(1, 0.45 + Math.sin(t * 1.7) * 0.25 + Math.sin(t * 4.1) * 0.15 + (Math.random() - 0.5) * 0.2))); }, 80); return () => { clearInterval(id); setLevel(0); }; }, [state]); const toggle = () => { timers.current.forEach(clearTimeout); if (state === "idle") { setState("listening"); timers.current.push( setTimeout(() => setState("thinking"), 3600), setTimeout(() => setState("speaking"), 5200), setTimeout(() => setState("idle"), 8600), ); } else setState("idle"); }; return ( <div className="grid min-w-0 gap-5 rounded-overlay border border-border p-5 sm:grid-cols-[auto_1fr] sm:items-center sm:p-8"> <VoiceOrb state={state} level={level} onToggle={toggle} /> <div className="grid gap-3"> <h3 className="text-sm font-medium">Voice mode</h3> <p className="text-sm text-muted-foreground">A simulated conversation: it listens, thinks, then speaks. Press the orb again to stop.</p> <div className="flex flex-wrap gap-2"> <Button variant="ghost" size="sm" onClick={() => setState("denied")}>Preview blocked</Button> <Button variant="ghost" size="sm" onClick={() => setState("unavailable")}>Preview unavailable</Button> <Button variant="ghost" size="sm" onClick={() => setState("idle")}>Reset</Button> </div> </div> </div> ); }
API reference #
VoiceOrb
component
PropTypeDefaultDescription
state *VoiceState—
levelnumber0
onToggle(() => void)—Called when the orb is pressed to start or end listening.
labelstring"Voice"
sizenumber144
classNamestring—
Accessibility #
It exposes status semantics, with aria-label, aria-pressed and aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.