Thinking Orbs
Install: npm install thinking-orbs@0.3.2
When to use
Render a compact visual state for a coding or chat agent.
Key native props
| Prop | Meaning |
|---|---|
state | Nine native literal states: working, searching, solving, listening, connecting, weaving, composing, breathing, shaping. |
size | 20, 32 or 64; 20 and 64 are tuned designs, 32 is interpolated. Arbitrary numeric sizes are not this API. |
paused | Freeze the current frame. speed, dots and dotSize tune the animation and density. |
theme | auto observes ancestor theme attributes/classes, while light/dark pin the palette. |
color | Optional ink tint; preserve a useful contrast against the transparent canvas background. |
Interaction and state checks
Switch every state, all three sizes, pause/resume and light/dark. Provide nearby semantic status text instead of relying on a canvas animation alone.
Integration boundaries
listening does not capture audio and searching does not call a search service. Advanced frame/opts/gravity are native escape hatches; they are not a guaranteed universal state-machine contract.
Example files and host setup
Copy the component file and shared.tsx from examples in the source panel, keeping their relative locations; do not copy only the upstream implementation. Install the pinned npm package and use its own export in a browser component. Examples need no Tailwind or competing theme provider, read host html language/theme and provide reduced-motion/graphics fallbacks. Native implementation references are for reading; import via npm rather than copying an incomplete engine. Showcase labels are not new native props. Upstream detailed prose remains English; no full-translation claim is made.
Runnable usage
"use client";
import { useState } from "react";
import { ThinkingOrb, type OrbState, type OrbSize } from "thinking-orbs";
import { DemoFrame, GraphicsGuard, control, demoTheme, t } from "./shared";
export default function Example({ variant = "Default" }: { variant?: string }) {
const states: OrbState[] = ["working", "searching", "solving", "listening", "connecting", "weaving", "composing", "breathing", "shaping"];
const [state, setState] = useState<OrbState>(states.find(s => s === variant.toLowerCase()) ?? "working"), [size, setSize] = useState<OrbSize>(64), [paused, setPaused] = useState(variant === "Paused");
return <DemoFrame status={`state=${state}, size=${size}, paused=${paused}`} controls={<>
<label>{t("State", "状态")} <select style={control} aria-label={t("Orb state", "光球状态")} value={state} onChange={e => setState(e.target.value as OrbState)}>{states.map(s => <option key={s}>{s}</option>)}</select></label>
<label>{t("Size", "尺寸")} <select style={control} aria-label={t("Orb size", "光球尺寸")} value={size} onChange={e => setSize(Number(e.target.value) as OrbSize)}>{[20, 32, 64].map(s => <option key={s}>{s}</option>)}</select></label>
<button type="button" style={control} aria-pressed={paused} onClick={() => setPaused(!paused)}>{paused ? t("Resume", "继续动画") : t("Pause", "暂停动画")}</button>
</>}><GraphicsGuard><ThinkingOrb state={state} size={size} theme={demoTheme()} paused={paused} aria-label={t(`Agent visual state: ${state}`, `智能体视觉状态:${state}`)} /></GraphicsGuard></DemoFrame>;
}
Preview states
Default, Searching, Solving, Listening, Connecting, Weaving, Composing, Breathing, Shaping, Paused
Demo state and mock data do not provide a backend service. Inspect the component props before wiring real data.
thinking-orbs
Dotted thought-orb loading indicators for AI & agent UIs. Nine hand-tuned 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 · Repository · Report an issue
Install
npm install thinking-orbs
Quick start
import { ThinkingOrb } from 'thinking-orbs';
function Status() {
return <ThinkingOrb state="searching" size={64} />;
}
States
Nine verbs an agent can be doing, each a distinct animation:
<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 */}
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
/>
All other <canvas> props (className, style, data-*, …) pass through.
Accessibility & performance
role="img"with a sensible per-statearia-labelout of the box.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 © Jakub Antalik
Source-backed parameter reference
Implementation signatures and types are generated from the pinned local source. Optional props, native inherited types, union branches and never exclusions are preserved. A forwardRef implementation's ref is a React reference, not an additional business prop.
ThinkingOrb({
state = 'working',
size = 64,
theme = 'auto',
speed = 1,
paused = false,
color,
dots = 1,
dotSize = 1,
opts: optsOverride,
frame: customFrame,
gravity,
style,
'aria-label': ariaLabel,
...rest
}: ThinkingOrbProps)
export type OrbState =
| 'working'
| 'searching'
| 'solving'
| 'listening'
| 'connecting'
| 'weaving'
| 'composing'
| 'breathing'
| 'shaping';
export type OrbSize = 64 | 32 | 20;
export type OrbTheme = 'auto' | 'dark' | 'light';
export interface ThinkingOrbProps extends Omit<CanvasHTMLAttributes<HTMLCanvasElement>, 'style'> {
/** Which animation to show. @default 'working' */
state?: OrbState;
/** Tuned size preset — 64 or 20 CSS px. @default 64 */
size?: OrbSize;
/** Theme mode; `auto` detects from the host project. @default 'auto' */
theme?: OrbTheme;
/**
* Animation speed multiplier on top of the preset's baked speed.
* @default 1
*/
speed?: number;
/** Freeze the animation on the current frame. @default false */
paused?: boolean;
/**
* Optional ink tint — any `#rgb`, `#rrggbb` or `rgb()` color. The orb's
* depth-shading ramp is preserved on the tint (fading toward black on
* dark substrates, toward white on light ones), so colored orbs read
* with the same 3D language as the grayscale default. Omit for the
* stock grayscale ink.
*/
color?: string;
/**
* Density multiplier for the mode's dot/strand/node counts, applied with
* the same paired-count scaler the size presets use so the mode keeps
* its balance. `1` (default) is the tuned look; `0.5` halves density,
* `2` doubles it. Clamped to a 0.1 floor.
*/
dots?: number;
/**
* Radius multiplier for every dot, applied with the same scaler the size
* presets use so near/far falloff keeps its proportion. `1` (default) is
* the tuned mark; `0.7` reads finer, `1.5` bolder.
*/
dotSize?: number;
/**
* Advanced: raw draw options merged over the resolved preset, last. These
* are the engine's own knobs, so they reach past the tuned surface — the
* orbit paths' `ghostA` and `particles` (working), the globe's `scanMul`
* and `dimBase` (searching), the constellation's `thr`, `signals`, `lineW`
* and `spread` (connecting), the plait's `turns` (weaving), the sash's
* `wobMul`, `bandMul` and `spin` (composing, breathing), and the outline's
* `spread` and `shape` (shaping — `shape` 0 / 1 / 2 holds the circle,
* triangle or square instead of cycling). Unknown keys are ignored; a key
* the current state does not read does nothing.
*/
opts?: ModeOpts;
/**
* Escape hatch below `opts`: replace the state's geometry outright. The
* function gets `(size, t, opts)` and returns the frame to paint — the
* same contract every built-in mode implements (see `MODE_FRAMES` and the
* helpers exported alongside it). The painter, theme, tint, pause and
* offscreen handling all stay the library's.
*/
frame?: ModeFrame;
/**
* Gravity: the orb pulls at the pointer. As the pointer nears, a raster
* of the platform's cursor is drawn in its place with its body warped
* toward the orb — the tip stays put, nothing rotates or scales. Needs the
* platform's real pointer raster (`gravity.sprite`, or
* `setGravitySprite`) or it stays off; off automatically under reduced
* motion, forced colours, coarse pointers, zoom, and over anything but
* the plain arrow. `true` takes the defaults. See gravity.ts.
*/
gravity?: boolean | GravityOptions;
style?: CSSProperties;
}
Destructuring defaults in source
Only defaults written directly in parameter/props destructuring are listed. Conditional expressions, runtime theme and computed values require source inspection; this is not an exhaustive behavior-default list.
// ThinkingOrb
state = 'working'
// ThinkingOrb
size = 64
// ThinkingOrb
theme = 'auto'
// ThinkingOrb
speed = 1
// ThinkingOrb
paused = false
// ThinkingOrb
dots = 1
// ThinkingOrb
dotSize = 1
Native and external types
External types remain defined by React or native dependencies, not manually expanded into guessed props. The installed version's TypeScript definitions are authoritative.
Omit<CanvasHTMLAttributes<HTMLCanvasElement>, 'style'>
CanvasHTMLAttributes<HTMLCanvasElement>
HTMLCanvasElement
ModeOpts
ModeFrame
GravityOptions
CSSProperties