@keniding/combobulating-orbs
v0.5.1
Published
Dotted thought-orb loading indicators for AI & agent UIs — fifteen states, two sizes, auto dark/light
Maintainers
Readme
combobulating-orbs
Dotted thought-orb loading indicators for AI & agent UIs. Fifteen animated states, each shipped at two purpose-tuned sizes, rendered on a plain 2D canvas — no WebGL, no filters, works identically in Chrome, Safari and Firefox.
Live demo · Storybook · Repository · Report an issue
Install
npm install @keniding/combobulating-orbsQuick start
import { ThinkingOrb } from '@keniding/combobulating-orbs';
function Status() {
return <ThinkingOrb state="searching" size={64} />;
}States
Eleven verbs an agent can be doing, each a distinct animation — plus four states that report the work itself:
<ThinkingOrb state="working" /> {/* particles on tilted orbits */}
<ThinkingOrb state="searching" /> {/* a scan meridian sweeps a dotted globe */}
<ThinkingOrb state="solving" /> {/* bands scramble, then click back solved */}
<ThinkingOrb state="listening" /> {/* a waveform rolls through the rings */}
<ThinkingOrb state="connecting" /> {/* a constellation wires itself */}
<ThinkingOrb state="weaving" /> {/* three strands plait around the sphere */}
<ThinkingOrb state="composing" /> {/* an undulating multi-band sash */}
<ThinkingOrb state="breathing" /> {/* a ring slowly morphing */}
<ThinkingOrb state="shaping" /> {/* dotted outline: circle → triangle → square */}
<ThinkingOrb state="simmering" /> {/* bubbles rise through a dotted vessel and burst */}
<ThinkingOrb state="crystallizing" /> {/* a faceted lattice grows edge by edge, then dissolves */}States that report the work
Not moods — facts about the task. They are honest by construction: progress never animates a value of its own, the results play once and come to rest, and awaiting deliberately looks like nothing is advancing.
<ThinkingOrb state="progress" progress={0.4} /> {/* a sphere filled to exactly 40% — pass a measured value */}
<ThinkingOrb state="done" onComplete={() => {}} /> {/* gathers into a ring, draws a check, rests */}
<ThinkingOrb state="error" onComplete={() => {}} /> {/* a shaky gather, draws a cross, rests */}
<ThinkingOrb state="awaiting" /> {/* calm: a slow pulse and a blinking caret, waiting on a person */}Sizes
Two tuned presets — separate designs, not a scale factor. 64 for chat-avatar scale, 20 for inline-text scale. Each carries its own dot count, dot size and speed tuning:
<ThinkingOrb state="working" size={64} />
<ThinkingOrb state="working" size={20} />Theme
Strictly monochrome — light ink for dark backgrounds, dark ink for light backgrounds — with the mode picked automatically from the host project:
<ThinkingOrb theme="auto" /> {/* default — detects from the project */}
<ThinkingOrb theme="dark" /> {/* pin: light dots for dark backgrounds */}
<ThinkingOrb theme="light" /> {/* pin: dark dots for light backgrounds */}auto resolves in three layers and updates live when any of them change:
- an ancestor
data-theme="dark|light"attribute ordark/lightclass (the Tailwind / shadcn convention), watched viaMutationObserver; - otherwise
prefers-color-scheme, subscribed for live OS theme switches; - SSR-safe — the canvas paints only on the client, after the theme has resolved.
Other props
<ThinkingOrb
state="solving"
size={20}
speed={1.5} // multiplier on the preset's baked speed
paused={false} // freeze on the current frame
aria-label="Analysing repository…" // overrides the per-state default
progress={0.4} // 0–1, for state="progress" only
onComplete={() => {}} // fires once when done / error finish playing
/>Variants
Retune a state without leaving its animation. Every field multiplies the shipped preset (an empty variant changes nothing); opts sets raw per-mode knobs such as turns (weaving), thr/signals (connecting), moveCount (solving) or wobMul (breathing):
<ThinkingOrb state="weaving" variant={{ speed: 2, opts: { turns: 5 } }} /> {/* whisking */}
<ThinkingOrb state="breathing" variant={{ speed: 0.45, opts: { wobMul: 0.2 } }} /> {/* contemplating */}The demo ships 108 ready-made variants — one per status verb ("Whirring", "Percolating", "Reticulating"…), see demo/lib/verbs.ts.
All other <canvas> props (className, style, data-*, …) pass through.
Accessibility & performance
role="img"with a sensible per-statearia-labelout of the box.state="progress"with aprogressvalue is a realrole="progressbar"(aria-valuenow/aria-valuetext), so assistive tech reads the measured value.- Pass
aria-labelwith what the agent is actually doing ("Searching the docs…"). If you show text next to the orb, mark itaria-hidden— the canvas already carries the name, and a visible label that rotates every few seconds shouldn't be re-announced. prefers-reduced-motion: reducerenders a static representative frame — no animation — and still follows the live theme.- Every instance pauses automatically when scrolled offscreen (
IntersectionObserver) or when the tab is hidden, and resumes in phase — all instances share one clock. - Plain 2D canvas arcs only: no
ctx.filter, no SVG filters, no WebGL — the same pixels everywhere, cheap on low-end devices. Device-pixel-ratio capped at 2.
License
MIT © Keniding — see LICENSE
