caliper uidocs

Voice orb

Free

An animated orb that shows idle, listening, thinking and speaking states.

Installs components/ui/voice-orb.tsxDepends on lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/voice-orb.json

Tap to talk

Voice mode

A simulated conversation: it listens, thinks, then speaks. Press the orb again to stop.

When 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.