Library / SDK
Jakubantalik/thinking-orbs avatar
Jakubantalik/thinking-orbs

thinking-orbs: nine canvas loading states for agent UIs, and where the preset system breaks down

Dotted thought-orb loading indicators for AI & agent UIs, 9 tuned types, two sizes, auto dark/light

2,771 stars216 forksTypeScriptMIT

At a glance

What is it?
A TypeScript React component that ships nine hand-tuned dotted loading animations at two fixed sizes, rendered on a 2D canvas with automatic theme resolution. The interesting design decision is that size is a preset, not a scale factor, which is also the main constraint.
Who is it for?
Adopt thinking-orbs if you are building a chat or agent interface in React, you want a loader whose animation actually names what the agent is doing, and you can live with two sizes. Do not adopt it if you need an arbitrary pixel size, a colour that is not monochrome ink, or a non-React renderer.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 31 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem is that a spinner says nothing about what the agent is doing

Most agent interfaces show one loading affordance for every wait. A tool call, a retrieval pass, a streaming completion and a socket reconnect all get the same rotating arc. The README frames thinking-orbs as a fix for that: nine named states, each described as a distinct animation rather than a recolour of the same one. Searching is documented as a scan meridian sweeping a dotted globe. Connecting is a constellation wiring itself. Solving shows bands scrambling and then clicking back solved.

The audience is narrow and identifiable. It is React developers building chat surfaces, agent dashboards or copilot panels who already treat the loading indicator as part of the product's voice. If your interface is a table of job statuses, this library is aimed at someone else. The value it claims is semantic: the animation carries the verb, so the user knows whether to wait two seconds or two minutes.

Nine states, and what each one actually animates

The README lists the states with a one-line description of the motion for each. Working is particles on tilted orbits. Searching is that scan meridian over a dotted globe. Solving scrambles bands and restores them. Listening rolls a waveform through the rings. Connecting draws a constellation. Weaving plaits three strands around the sphere. Composing is an undulating multi-band sash. Breathing is a single ring slowly morphing. Shaping cycles a dotted outline through circle, triangle and square.

That is the whole vocabulary, and it is a deliberate limit. There is no way to define a tenth state from the public API shown in the README, and no documented callback or frame hook. You pick from the nine or you do not use the component. The descriptions are also the only specification available: the README does not state frame rates, dot counts per state, or durations, so tuning decisions have to be made by eye against the live demo rather than from numbers.

Size is a preset, not a scale factor, and that is the sharpest constraint

The component accepts size={64} and size={20}. The README is explicit that these are separate designs rather than one drawing scaled up or down, and that each carries its own dot count, dot size and speed tuning. 64 is described as chat-avatar scale, 20 as inline-text scale.

This is the design choice that most affects whether you can adopt the library. A hand-tuned 20px variant will look better than a downscaled 64px one, because dot spacing and motion amplitude do not survive naive scaling. The cost is that there is no documented path to 32px, 48px or 96px. If your layout needs an orb at a size the two presets do not cover, the README offers no answer, and the props list contains no dot count, radius or spacing override. You would be scaling a canvas element yourself, which reintroduces exactly the problem the presets exist to avoid.

Theme resolution runs in three layers and repaints live

The component is monochrome by design: light ink on dark backgrounds, dark ink on light backgrounds. theme="auto" is the default and resolves in three ordered layers. First it looks for an ancestor with a data-theme="dark|light" attribute or a dark/light class, which the README identifies as the Tailwind and shadcn convention, and watches it with a MutationObserver. Failing that it falls back to prefers-color-scheme and subscribes to OS theme changes. The third layer is SSR safety: the canvas paints on the client only, after the theme has resolved.

That ordering matters in practice. If your application toggles themes by swapping a class on a wrapper element, auto should track it without configuration. If it toggles by writing a cookie and re-rendering a stylesheet, the MutationObserver will not see anything, and the component will fall through to the OS preference, which may disagree with your page. theme="dark" and theme="light" pin the ink and skip the detection entirely, and that is the honest escape hatch when your theming does not match either of the first two layers.

Getting it running: install, one import, four props

Installation is a single npm command:

npm install thinking-orbs

The README's quick start imports the named component and renders it with a state and a size:

import { ThinkingOrb } from 'thinking-orbs';

<ThinkingOrb state="searching" size={64} />

Beyond state and size, the documented props are speed, a multiplier on the preset's baked speed; paused, which freezes on the current frame; and aria-label, which overrides the per-state default. theme takes auto, dark or light. All other canvas props, including className, style and data-* attributes, pass through to the underlying element, so positioning and layout stay in your hands.

There is no provider, no context, no stylesheet import and no build step described in the README. The component is TypeScript, the package is published on npm, and the repository's default branch is main. No release notes were retrieved for this review, so version history and any breaking changes between versions cannot be assessed from the material available.

Accessibility and the shared clock are the parts worth copying

The component sets role="img" with a per-state aria-label by default, and the README gives the example aria-label="Analysing repository…" for an override. Under prefers-reduced-motion: reduce it renders a static representative frame with no animation, and the README states that this frame still follows the live theme. That is a more careful treatment than most loaders manage, where reduced motion either kills the indicator entirely or leaves it spinning.

The performance story rests on two mechanisms. Every instance pauses when scrolled offscreen via IntersectionObserver or when the tab is hidden, and resumes in phase because all instances share one clock. Rendering is plain 2D canvas arcs, with no ctx.filter, no SVG filters and no WebGL, and device-pixel-ratio is capped at 2. The README's claim is identical pixels across Chrome, Safari and Firefox, which is plausible given the restricted drawing surface, but this review has not verified rendering in any browser.

Where it is the wrong tool

The monochrome rule is the first hard limit. There is no documented colour prop. If your design system needs the loader in a brand accent, a success green or a warning amber, this component does not offer it, and the README presents the constraint as intentional rather than as a gap. A coloured orb would have to come from somewhere else.

The second limit is the fixed state list. Nine verbs cover a lot of agent activity, but the moment your product needs a state that is not one of them, you are back to a generic spinner next to eight bespoke ones, which looks worse than using a generic spinner everywhere. There is no documented composition API for building a tenth animation from the same primitives.

The third is the size preset, described above. The fourth is React: the quick start is JSX and the package is TypeScript, and the README describes no framework-agnostic entry point. If you render with Svelte, Vue or plain DOM, the canvas drawing code exists but the documented interface does not reach you.

What a CSS or SVG loader does differently

The obvious alternative is a CSS keyframe loader, or an animated SVG in the style of the spinners that ship with icon sets. The difference is not quality, it is where the work happens. A CSS loader animates a handful of DOM elements with transform and opacity, so it inherits your CSS custom properties, your colour tokens and any size you give it, and it costs no JavaScript beyond the markup.

thinking-orbs takes the opposite route. It draws arcs onto a canvas from TypeScript, which is what makes the nine states possible at all: a scan meridian over a dotted globe, or three strands plaiting around a sphere, is not something you want to express as a stack of divs with keyframes. The trade is that you give up CSS-level theming, arbitrary sizing and framework independence in exchange for motion that a stylesheet cannot reasonably produce. If your loader only needs to rotate, a CSS spinner is smaller, more themeable and easier to reason about. If it needs to say searching rather than busy, canvas is the more direct tool.

Maintenance, licensing and what to check before adopting

The licence is MIT, copyright Jakub Antalik. That permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. This is a description of the licence text, not legal advice; if your organisation has rules about attribution in shipped bundles, confirm them against the actual LICENSE file in the repository rather than against this summary.

On maintenance, the material supports only a thin assessment. The last push to the default branch is dated 2026-08-16 and the repository is not archived. No releases were retrieved, so there is no version history to inspect for breaking changes, deprecation notices or a stated support policy. The README documents no peer dependency range, so React version compatibility has to be checked in the package manifest before you install.

The upgrade cost is likely to be low as long as the public surface stays at state, size, theme, speed, paused and aria-label. The risk sits in the animations themselves. Because the presets are hand-tuned rather than derived, any visual adjustment to a state is a change to the component's output, and there is no documented visual regression suite or snapshot of the rendered frames to compare against. If a future version retunes the dots, you will find out by looking at your own screen.

Editorial conclusion

Adopt thinking-orbs if you are building a chat or agent interface in React, you want a loader whose animation actually names what the agent is doing, and you can live with two sizes. Do not adopt it if you need an arbitrary pixel size, a colour that is not monochrome ink, or a non-React renderer. Before wiring it in, open the live demo at orbs.jakubantalik.com and check two things in your own browser: that the state you picked reads as the verb you mean at 20px, and that your app's theme switching happens through a data-theme attribute or a dark/light class, because that is the first layer the auto resolver looks at and the one most likely to differ from your setup.

Official sources

  1. Issues
  2. Jakubantalik/thinking-orbs on GitHub
  3. License: MIT
  4. README
Community notes

Community notes