Skip to content
Expo Development Foundation
Esc
navigateopen⌘Jpreview
On this page

AI loading indicator

Libraries.dev thought-orbs behind a 15-line theme adapter. Single-source provisional.

Provisional — single source. This pattern ships in one production app so far plus the external procedure below. It earns full status when a second app adopts it. There is no showcase/ demo: the native port needs Skia, which is outside the fixture’s Expo Go contract.

Ship Libraries.dev dotted thought-orbs as the loading indicator in AI or agent “thinking” slots. Do not rebuild the animation. Do not leave ActivityIndicator in thinking slots.

When to use

  • AI/agent wait states: working, searching, solving, composing, and the other states in the table below.
  • Chat rows (size 64 beside status text), inline status (size 20 in the sentence), full-screen waits (centered 64).

When not to use

  • Determinate progress (downloads, uploads, installs) — use a progress bar.
  • Non-AI spinners already covered by platform indicators with no “thinking” semantics.

File map

src/components/thinking-orb.tsx      # 15-line adapter (native)
src/components/thinking-orb.web.tsx  # 15-line adapter (web, DOM renderer)

Install first, then vendor the port into the consumer repo (never assume a shared packages/ port exists):

npx expo install thinking-orbs-native
# web renderer for the .web.tsx file
npm i thinking-orbs

Full procedure: the thinking-orbs skill (https://github.com/Jakubantalik/Libraries.dev, playground https://libraries.dev/orbs).

Key excerpts (complete — the file is the pattern)

import { ThinkingOrb } from 'thinking-orbs-native';
import type { OrbSize, OrbState } from 'thinking-orbs-native';
import { useEffectiveColorScheme } from '../theme';

export type { OrbSize, OrbState };

export function LoadingOrb({ state, size }: { state: OrbState; size: OrbSize }) {
  // Follows the effective appearance via useEffectiveColorScheme().
  // Hide when not loading is handled by the caller (conditional render).
  // No extra spin/opacity wrappers — reduced-motion freezes at t=0.6 internally.
  const effective = useEffectiveColorScheme();
  return <ThinkingOrb state={state} size={size} theme={effective} />;
}

The .web.tsx twin imports from thinking-orbs instead — same surface.

Contract (from the skill — do not improvise past this)

  • Nine states: working, searching, solving, listening, connecting, weaving, composing, breathing (default for unknown AI wait), shaping.
  • Two tuned sizes, not a scale factor: 64 (avatar, hero, overlay) and 20 (inline). For other on-screen sizes keep the preset and pass displaySize; never transform-scale a rasterized orb.
  • Theme 'auto' | 'dark' | 'light'; pass the app’s in-app toggle explicitly when one exists. Web-only props (color, dots, dotSize, opts, frame, gravity, size 32) must not be passed on native.
  • Built-in per-state accessibility labels and reduced-motion freeze — do not wrap the orb in extra animation loops. Replace the spinner in the same slot; keep neighboring copy; hide when not loading.

Anti-patterns

  • Rebuilding the orb animation by hand instead of vendoring the port.
  • Leaving ActivityIndicator / ProgressView in thinking slots.
  • Passing web-only props on native, or transform-scaling the orb.
  • Confirming the import is thinking-orbs-native / ThinkingOrbsKit, not a hand-rolled canvas — and that Skia/Reanimated (native) or the SPM package (Swift) actually resolve.

Provenance

Application labels are anonymized because the production source checkout is private. They document observed SDK versions and file shapes, not a publicly reproducible source.

  • Production Expo 57 app B src/components/thinking-orb.tsx + thinking-orb.web.tsx (only shipping app so far).
  • External thinking-orbs skill: source https://github.com/Jakubantalik/Libraries.dev (packages/thinking-orbs), playground https://libraries.dev/orbs.

Was this page helpful?