nativeorbs
v0.0.2
Published
Dotted thinking-orb loading indicators for React Native + Expo AI/agent UIs — nine animated states, Skia-rendered. A port of thinking-orbs.
Maintainers
Readme
nativeorbs
Dotted thinking-orb loading indicators for React Native + Expo — for AI & agent UIs. Nine hand-tuned animated states, each an honestly-3D dotted sphere, rendered on Skia. Monochrome, theme-aware, two tuned sizes.
nativeorbs is a React Native port of thinking-orbs: the entire animation engine (nine mode painters, density profiles, presets, and the shared 3D/noise math) ports near-verbatim; the pixel layer is rebuilt on @shopify/react-native-skia.
import { ThinkingOrb } from 'nativeorbs'
<ThinkingOrb state="searching" size={64} />Install
Skia is a native module — nativeorbs runs on an Expo dev client / prebuild / EAS build, not Expo Go.
npm install nativeorbs
npx expo install @shopify/react-native-skia@shopify/react-native-skia (≥1.5) is the only peer — no Reanimated / Gesture Handler.
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 in quarter turns, then click back */}
<ThinkingOrb state="listening" /> {/* a waveform rolls through latitude rings */}
<ThinkingOrb state="connecting" /> {/* a constellation wires itself, packets on the edges */}
<ThinkingOrb state="weaving" /> {/* three strands plait around the sphere */}
<ThinkingOrb state="composing" /> {/* an undulating multi-band sash */}
<ThinkingOrb state="breathing" /> {/* a face-on ring slowly morphing */}
<ThinkingOrb state="shaping" /> {/* a dotted outline: circle → triangle → square */}Each ships a sensible per-state accessibilityLabel (e.g. "Searching…"); override with the prop.
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 dots on dark backgrounds, dark dots on light — with the mode following the device color scheme by default:
<ThinkingOrb theme="auto" /> {/* default — follows useColorScheme(), live */}
<ThinkingOrb theme="dark" /> {/* pin: light dots (for dark backgrounds) */}
<ThinkingOrb theme="light" /> {/* pin: dark dots (for light backgrounds) */}The canvas is transparent, so an orb sits on any background.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| state | OrbState | 'working' | Which of the nine animations to show. |
| size | 64 \| 20 | 64 | Tuned size preset. |
| theme | 'auto' \| 'dark' \| 'light' | 'auto' | Dot palette; auto follows the OS scheme. |
| speed | number | 1 | Multiplier on the preset's baked speed. |
| paused | boolean | false | Freeze on the current frame. |
| accessibilityLabel | string | per-state | Overrides the default label. |
| style | ViewStyle | — | Style on the wrapping view. |
Accessibility & performance
accessibilityRole="image"with a per-stateaccessibilityLabelout of the box.- Reduced motion (
AccessibilityInfo) renders a single static representative frame — no loop — and still follows the theme. - One shared wall clock keeps every mounted orb in phase.
- Plain Skia arcs only — no blur, no filters — cheap on low-end devices.
Power-user surface
Everything the component uses is exported, for driving your own Skia canvas:
import { resolvePreset, MODE_DRAWS, paint, type Dot } from 'nativeorbs'
const { mode, opts, speed } = resolvePreset('searching', 64)
// MODE_DRAWS[mode](skCanvas, size, t, dark, opts) — draw a frame yourselfHow it works
state → a mode painter + fully-scaled options (resolvePreset, cached). Each painter computes an array of depth-sorted dots (and, for connecting, edges) on a rotated 3D sphere, then hands them to paint() — the only pixel code, which draws grayscale Skia circles with depth carried by size and ink weight. A requestAnimationFrame loop records one SkPicture per frame.
The engine (mode painters, profiles, presets, 3D/noise math) is ported near-verbatim from thinking-orbs; only paint/paintLines and the component are Skia.
Development
npm install
npm test # vitest — preset resolution + scaling machinery
npm run typecheck
npm run build # tsup → dist (ESM + CJS + d.ts)License
MIT. Ported from thinking-orbs (MIT, © Jakub Antalik) — see LICENSE.
