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.

Six states, each a distinct silhouette in motion. Every dot is drawn on the UI thread.

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-orb

Usage

<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

PropTypeDefaultDescription
classNamestring
stateThinkingOrbState'working'Which of the six animations to show.
sizenumber64Side 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.
speednumber1Multiplier on the state's own speed.
pausedbooleanfalseFreeze on the current frame.
colorstringInk colour. Defaults to the theme's foreground, so the orb inverts with it.
accessibilityLabelstringOverrides 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.

On this page