60 / Agent UI
Orb
A sphere that carries what an assistant is doing, settled when idle and moving with the voice when there is one.
"use client"
import * as React from "react"
import { DemoVariants } from "@/components/demos/demo-variants"
import { Orb, type OrbState } from "mischief-ui/orb"
const states: OrbState[] = ["idle", "listening", "thinking", "speaking"]
function Live({ state }: { state: OrbState }) {
const [level, setLevel] = React.useState(0)
React.useEffect(() => {
if (state !== "listening" && state !== "speaking") return
// A stand-in for a real level, so the demo shows what one looks like.
const timer = setInterval(() => setLevel(0.25 + Math.random() * 0.7), 140)
return () => clearInterval(timer)
}, [state])
return <Orb state={state} level={level} size={140} />
}
export function OrbDemo() {
return (
<DemoVariants
label="State"
variants={
states.map((state) => ({
id: state,
label: state,
render: () => <Live state={state} />,
})) as [
{ id: string; label: string; render: () => React.ReactNode },
...{ id: string; label: string; render: () => React.ReactNode }[],
]
}
/>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/orbimport { Orb } from "mischief-ui/orb"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 OrbState = "idle" | "listening" | "thinking" | "speaking" export type OrbProps = Omit<Usage
export function Assistant() {
return <Orb state={state} level={level} />
}Four states, four paces
Idle settles, listening breathes, thinking turns over faster, and speaking moves with the voice. The pace is the difference: a reader learns which is which in a few seconds without a legend.
Only listening and speaking read the level. The other two keep their own rhythm, so a stale level left over from a finished turn cannot make a thinking orb pulse as though somebody were talking.
Feeding it a level
level is a number between nought and one, and it is read inside the frame loop rather than through a re-render. Handing it sixty values a second from an analyser costs one canvas frame, not sixty renders of the tree around it.
API
state"idle" | "listening" | "thinking" | "speaking"What the assistant is doing. Defaults to "idle".levelnumberHow loud it is now, nought to one.colorstringA theme token or CSS colour. Defaults to "--primary".sizenumberWidth and height in pixels. Defaults to 120.labelstringSaid aloud in place of the state name.Accessibility
The orb is a drawing, so what it means is also written: the state, or a label of your own, is announced through a polite live region. Nothing about the assistant's condition is carried by the animation alone, which matters because the animation is exactly what a reduced-motion setting or a sleeping surface will take away. The canvas itself is hidden from assistive technology.