Components

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.

Updated Oct 6, 2026 by Juan Pablo Turina

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.

  1. 1
    Ball

    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.

  2. 2
    Halo

    A soft ring that leaves the orb: every 2.8s while attentive, every 1.4s while searching, once at the end of done.

  3. 3
    Ring

    A 1px silver line with two glints. It breathes while answering and goes out as a wave at the end of done.

  4. 4
    Warning 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.

120px
60px
32px
20px
16px
<AgentOrb size={120} />
<AgentOrb size={60} />
<AgentOrb size={32} />
<AgentOrb size={20} />  {/* still: drawn once */}
<AgentOrb size={16} />  {/* still */}

Recipes

Next to the composer (canonical)

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>
    </>
  );
}
A still mark beside the agent's name

In a message, a card or a list, the orb at 20px marks what the agent wrote. It is drawn once and never animates.

YunoApproval rate fell 2.1 points this week.
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>
Waiting for a decision

Needs you, next to the line that says what decision it is waiting for. The dot alone never says what to do.

Approve the new route before it goes live.
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.

AgentOrb draws into a canvas with WebGL and falls back to CSS on its own. Give it the state your agent is in; everything else has a default. It is aria-hidden: put the state in words next to it (visible or aria-live) where it matters.
import { AgentOrb, type AgentOrbState } from "@/components/ui/agent-orb";

Props

Everything else from the underlying HTML or Radix primitive is forwarded via ...props.

PropTypeDefaultDescription
state"idle" | "attentive" | "searching" | "answering" | "done" | "needs-you" | "paused" | "leaving""idle"What the agent is doing right now.
sizenumber60Diameter in px.
followbooleansize >= 32Lean toward the pointer.
stillbooleansize <= 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.
palettestring[]—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.
classNamestring—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.