Agent orb
The face of the agent: a wave inside a glass ball (the Esfera). It is the one thing on screen that says the agent is working, and what it is doing: listening, looking something up, answering, done, waiting for you. One component everywhere the agent appears, so it is always the same object.
Anatomy
A ball drawn by WebGL into a canvas, and around it the gestures the CSS draws: a halo, a ring and a warning dot. Where WebGL is missing the ball is drawn in CSS instead, with the same gestures.
- 1Ball
The wave inside the glass, drawn by one shared WebGL renderer for every orb on the page. Its light leans toward the pointer; it lights up while answering and floods once when done. Without WebGL, a CSS face derived from --primary: pale above, vivid blue below, a light band where the wave runs.
- 2Halo
A soft ring that leaves the orb: every 2.8s while attentive, every 1.4s while searching, once at the end of done.
- 3Ring
A 1px silver line with two glints. It breathes while answering and goes out as a wave at the end of done.
- 4Warning dot
Needs you: a --warning dot on the top right, with a --card border so it reads on any surface.
States
Eight states. Set the one that matches what the agent is doing right now; the orb never moves on a timer of its own. Done plays once, about 3s: go back to idle after it. Leaving fades the orb out. With reduced motion nothing moves and each state still reads: the ball shows its state in one frame and the halo or ring holds still.
Searching. The data is still coming back. The wave speeds up and a short halo goes out every 1.4s.
<AgentOrb state="searching" />Without WebGL
Some browsers draw nothing with WebGL: hardware acceleration switched off, virtual machines and remote desktops, screenshot tools, or a page with so many WebGL canvases that the browser takes the orb's back. There the orb draws its ball in CSS and keeps every gesture, so each state still reads. renderer="css" forces it, which is what this comparison does.
WebGL
Without WebGL (renderer="css")
<AgentOrb state="answering" />
<AgentOrb state="answering" renderer="css" />Sizes
Any size in px. From 32px the orb leans toward the pointer. At 20px and under, at rest, it is a still mark: the same sphere drawn once, with no frame loop, so a list of marks costs nothing.
<AgentOrb size={120} />
<AgentOrb size={60} />
<AgentOrb size={32} />
<AgentOrb size={20} /> {/* still: drawn once */}
<AgentOrb size={16} /> {/* still */}Recipes
The orb follows the conversation: attentive while you type, searching while the data comes back, answering while the answer arrives, done for one breath, then idle. Type something and send it. Say the state in words for screen readers too: the orb is aria-hidden.
Idle. Nothing is happening. The wave drifts slowly, no halo.
import * as React from "react";
import { AgentOrb, type AgentOrbState } from "@/components/ui/agent-orb";
import { Textarea } from "@/components/ui/textarea";
// phase comes from your agent: "searching" while the data comes back,
// "answering" while the answer arrives, "done" once, then "idle".
export function Composer({ phase }: { phase: AgentOrbState }) {
const [draft, setDraft] = React.useState("");
const state: AgentOrbState = phase !== "idle" ? phase : draft.trim() ? "attentive" : "idle";
return (
<>
<div className="flex items-end gap-3">
<AgentOrb size={40} state={state} />
<Textarea value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Ask Yuno anything" />
</div>
<p className="sr-only" aria-live="polite">{state === "searching" ? "Looking it up" : ""}</p>
</>
);
}In a message, a card or a list, the orb at 20px marks what the agent wrote. It is drawn once and never animates.
import { AgentOrb } from "@/components/ui/agent-orb";
<div className="flex items-center gap-2">
<AgentOrb size={20} />
<span className="text-sm font-medium">Yuno</span>
<span className="text-sm text-muted-foreground">Approval rate fell 2.1 points this week.</span>
</div>Needs you, next to the line that says what decision it is waiting for. The dot alone never says what to do.
import { AgentOrb } from "@/components/ui/agent-orb";
<div className="flex items-center gap-3">
<AgentOrb size={32} state="needs-you" />
<span className="text-sm">Approve the new route before it goes live.</span>
</div>Import
Copy this import line at the top of the file where you compose this atom.
import { AgentOrb, type AgentOrbState } from "@/components/ui/agent-orb";Props
Everything else from the underlying HTML or Radix primitive is forwarded via ...props.
| Prop | Type | Default | Description |
|---|---|---|---|
| state | "idle" | "attentive" | "searching" | "answering" | "done" | "needs-you" | "paused" | "leaving" | "idle" | What the agent is doing right now. |
| size | number | 60 | Diameter in px. |
| follow | boolean | size >= 32 | Lean toward the pointer. |
| still | boolean | size <= 20 && state === "idle" | Draw the resting sphere once, with no frame loop. A still orb is always at rest. |
| renderer | "webgl" | "css" | "webgl" | "webgl" falls back to CSS where WebGL is missing; "css" always draws the CSS face. |
| palette | string[] | — | Three or more hex colours (not tokens) that recolour the wave of the WebGL ball; the CSS face keeps --primary. Leave it out to use the sphere's own blue. |
| className | string | — | Extra classes on the orb, for placement. Size it with size, not with a class. |
Related
Cross-links to atoms and patterns you may reach for next.
- Suggestion chipThe suggestions under the composer the orb sits next to.
- TextareaThe composer box.
- MotionThe orb moves on its own timing, listed there under Outside the tokens.