JGuang

Thinking Orbs

Thinking Orbs · Libraries.dev

Preview
npm install thinking-orbs@0.3.2

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

PropMeaning
stateNine native literal states: working, searching, solving, listening, connecting, weaving, composing, breathing, shaping.
size20, 32 or 64; 20 and 64 are tuned designs, 32 is interpolated. Arbitrary numeric sizes are not this API.
pausedFreeze the current frame. speed, dots and dotSize tune the animation and density.
themeauto observes ancestor theme attributes/classes, while light/dark pin the palette.
colorOptional 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:

  1. an ancestor data-theme="dark|light" attribute or dark/light class (the Tailwind / shadcn convention), watched via MutationObserver;
  2. otherwise prefers-color-scheme, subscribed for live OS theme switches;
  3. 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-state aria-label out of the box.
  • prefers-reduced-motion: reduce renders 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