ThinkingOrb
Dotted orb saying which kind of busy an agent is.
A spinner says “busy”. This says which kind of busy: particles running tilted orbits for work in flight, a scan meridian sweeping a globe for a search, bands that scramble and click back for a solve. Six states, each a distinct silhouette in motion, so a glance is enough.
Installation
ThinkingOrb ships with the library — no separate install.
import { ThinkingOrb, Message, Shimmer, Text } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add thinking-orbUsage
<ThinkingOrb state="searching" />
// The small tuning, for a line of text.
<ThinkingOrb state="working" size={20} />Examples
The six states
Pick the one that matches what is actually happening. The states are not interchangeable decoration — the whole reason to use this instead of a spinner is that the shape in motion tells the reader something, and a solving orb over a network request is a lie.
<ThinkingOrb state="working" /> {/* running a task */}
<ThinkingOrb state="searching" /> {/* looking something up */}
<ThinkingOrb state="solving" /> {/* working a problem out */}
<ThinkingOrb state="listening" /> {/* taking input */}
<ThinkingOrb state="composing" /> {/* writing a reply */}
<ThinkingOrb state="shaping" /> {/* forming a structure */}Inline in a streaming reply
At or below 32px the orb switches to a different tuning — far fewer, proportionally much larger dots, moving faster. It is a separate design, not the large one scaled down, because a faithful lattice at that size is grey mush. Pair it with Shimmer and the two say the same thing in two registers.
<Message>
<Message.Avatar>
<Avatar size="sm" fallback="AI" />
</Message.Avatar>
<Message.Content>
<Message.Bubble>
<View className="flex-row items-center gap-2">
<ThinkingOrb state="searching" size={20} />
<Shimmer>Searching the docs…</Shimmer>
</View>
</Message.Bubble>
</Message.Content>
</Message>Speed, pause and colour
speed multiplies the state's own tuning and can change mid-animation without the orb jumping. paused holds the current frame rather than clearing it — a still orb is not an empty one. color overrides the ink, which is otherwise --color-foreground so the orb inverts with the theme.
<ThinkingOrb state="working" speed={1.6} />
<ThinkingOrb state="working" paused={busy === false} />
<ThinkingOrb state="composing" color="#22c55e" />Versions
The six states
Each one side by side at the large tuning, so the silhouettes can be compared. They are not interchangeable decoration — the shape in motion is the message.
{["working", "searching", "solving", "listening", "composing", "shaping"].map((state) => (
<Item key={state}>
<Item.Media>
<ThinkingOrb state={state} size={56} />
</Item.Media>
<Item.Content>
<Item.Title>{state}</Item.Title>
</Item.Content>
</Item>
))}Inline in a reply
The small tuning, in a line of chat text beside a Shimmer. Below 32px the orb switches to far fewer, much larger dots moving faster — the motion has to read at a size where the dots barely do.
<Message.Bubble>
<View className="flex-row items-center gap-2">
<ThinkingOrb state="searching" size={20} />
<Shimmer>Searching the docs…</Shimmer>
</View>
</Message.Bubble>Speed and pause
speed multiplies the state's own tuning and can change mid-animation without the orb jumping. paused holds the current frame rather than clearing it — a still orb is not an empty one.
const [speed, setSpeed] = useState(1);
const [paused, setPaused] = useState(false);
<ThinkingOrb state="working" size={140} speed={speed} paused={paused} />
<Slider min={0.2} max={3} step={0.1} value={speed} onValueChange={setSpeed} />
<Switch value={paused} onValueChange={setPaused} />API Reference
ThinkingOrb
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
state | ThinkingOrbState | 'working' | Which of the six animations to show. |
size | number | 64 | Side of the orb in pixels. Two tunings ship, and they are separate designs rather than one scaled: at or below 32 the orb switches to far fewer, proportionally much larger dots moving faster, because a faithful lattice at that size is grey mush. |
speed | number | 1 | Multiplier on the state's own speed. |
paused | boolean | false | Freeze on the current frame. |
color | string | — | Ink colour. Defaults to the theme's foreground, so the orb inverts with it. |
accessibilityLabel | string | — | Overrides the per-state default announced to screen readers. |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
How it is drawn
The geometry is honestly three-dimensional — points on a sphere, rotated and tilted, projected orthographically, with depth carried by dot size and ink weight. React Native has no 2D canvas to paint that into, and one animated SVG node per dot would be two hundred native prop writes a frame, which no amount of tuning survives.
So the dots are quantised into eight ink levels and each level is emitted as a single path of circle arcs. Eight animated props a frame whatever the dot count, and depth ordering comes free — depth is what drives the ink in the first place, so painting faint to strong paints far to near. Everything from the trigonometry to the path strings runs in one worklet on the UI thread; React renders once and then never again.
Size is two designs, not a scale
Two tunings ship. At or below 32px the orb uses far fewer, proportionally much larger dots moving faster, because the motion has to read at a size where the individual dots barely do. Sizes in between are scaled from the nearer tuning; the dot radii scale sub-linearly, which is what keeps a small orb from becoming a smudge and a large one from becoming beads.
Motion and accessibility
role="img" with a per-state label out of the box; accessibilityLabel overrides it with something more specific — “Analysing the repository…” beats “Searching”. Under prefers-reduced-motion the orb renders one representative frame and stops, which is why a paused orb still shows a shape.