A tiny SVG orb that shows what your AI assistant is actually doing.
Thinking ยท working ยท waiting on you ยท done. Zero dependencies, 4 KB.
The hard part of AI UX isn't the loading โ it's that every kind of waiting looks identical.
| agent-orb | Spinner | Unread badge | Lottie animation | |
|---|---|---|---|---|
| Tells thinking vs working | โ | โ | โ | โ |
| Shows "I'm waiting on you" | โ | โ | โ | โ |
| Survives a 60s image gen | โ | ๐ต feels frozen | โ | โ |
| Zero dependencies | โ | โ | โ | โ ~250 KB lib |
| Themeable via CSS vars | โ | โ | โ | โ re-export JSON |
| Respects reduced-motion | โ | โ |
A spinner says "something is happening." An orb says which something โ and when the model has stopped and is quietly waiting for your answer, it says that too. That last state is the one users miss most: the assistant asked a question, they walked away, and nothing on screen admits it's stuck.
-
๐ง Six states, each earned โ
idlethinkingworkingaskingdoneerror. No decorative extras: every state maps to something your app actually knows. - ๐ It looks at you โ eyes track the cursor with 1.6px of travel. Small enough that nobody calls it an animation, big enough that everybody feels it.
-
๐ชถ 4 KB, zero deps โ plain SVG + vanilla JS. Works in React, Vue, Svelte,
or a bare
<script type="module">. - ๐จ Themed with CSS variables โ five lines to recolor. No build step, no config file.
-
โฟ Accessible by default โ every state carries an
aria-label, andprefers-reduced-motionstops all animation while keeping the state readable.
npm i ai-orbimport { AgentOrb } from 'ai-orb'
const orb = new AgentOrb(document.querySelector('#orb'))
orb.state = 'thinking' // model is composing
orb.state = 'working' // long job running โ the ring spins
orb.set('done', 3) // finished, with an unread badge
orb.destroy() // always clean up on unmountVue 3:
<script setup>
import { AgentOrb } from 'ai-orb/vue'
</script>
<template>
<AgentOrb :state="state" :unseen="n" @click="openChat" />
</template>That's the whole API.
| State | When to set it | What it does |
|---|---|---|
idle |
Default | Breathes slowly. Nothing else โ it's always on screen. |
thinking |
Request sent, no tokens yet | Faster breathing, eyes drift up, three dots pulse. |
working |
A long task is running (image gen, tool call) | A ring orbits. The only continuous spin in the library. |
asking |
Assistant asked something and is blocked on you | Rocks gently, like a wave at the edge of your vision. |
done |
Result delivered while the panel is collapsed | Pops once, smiles, shows the badge. Then settles. |
error |
Request failed | Fades to grey. No flashing red โ that just makes people tense. |
Unknown values fall back to idle instead of throwing. State usually comes from a
backend, and one typo shouldn't blank the page.
Everything is a CSS variable. Override them on the element (or any ancestor):
.agent-orb {
--ao-size: 64px;
--ao-hi: #a5e9ff; /* inner highlight */
--ao-mid: #3aa8d8; /* body */
--ao-lo: #1c6a94; /* outer edge */
--ao-glow: #3aa8d8; /* halo */
--ao-ink: #0d2733; /* eyes */
}| Variable | Default | Purpose |
|---|---|---|
--ao-size |
56px |
Overall size |
--ao-hi / --ao-mid / --ao-lo
|
warm orange | Radial gradient, inner โ outer |
--ao-glow |
#ff8a50 |
Halo colour (states override this) |
--ao-ink |
#2a1a10 |
Eye colour |
--ao-badge-bg / --ao-badge-fg / --ao-badge-ring
|
โ | Unread badge |
host is a mount point โ the orb is appended to it, not swapped in.
Throws if host is missing (silent failure gets misdiagnosed as a CSS problem).
| Option | Type | Default | Notes |
|---|---|---|---|
state |
OrbState |
'idle' |
Unknown values fall back to idle
|
unseen |
number |
0 |
Badge shows only when state === 'done' and > 0
|
size |
number | string |
โ | Numbers are treated as px |
follow |
boolean |
true |
Cursor tracking |
blink |
boolean |
true |
Blinking |
labels |
object |
English | Override aria-label text for i18n |
onClick |
function |
โ | Click handler |
Instance: orb.state, orb.unseen (both settable), orb.set(state, unseen?),
orb.destroy(), orb.el (the button โ use it to set CSS variables).
Also exported: STATES, DEFAULT_LABELS, css, injectStyle(doc?).
Styles auto-inject once per page. Import css instead if you manage styles
yourself (SSR, strict CSP, or a custom theme pipeline).
Three deliberate limits โ they're choices, not an unfinished roadmap:
- Only six states. More states you can't tell apart at 56px is worse than fewer you can.
-
Only one thing spins. The ring is reserved for
working, the longest wait. If everything moved, nothing would read as urgent. - No cartoon face. It has to sit in the corner of a serious product without becoming the loudest thing on screen.
Multiple orbs on one page share a single mousemove listener and one requestAnimationFrame
loop; the last destroy() unbinds both. Gradient IDs are per-instance โ with a shared ID,
destroying the first orb turns every other one into an empty circle.
git clone https://github.com/webkubor/ai-orb
cd ai-orb
npm install
npm test
npx serve . # then open http://localhost:3000/demo/The demo imports the source directly โ no build step. It must be served over HTTP:
ES modules are blocked on file:// by CORS, which shows up as "the page loads but
there are no orbs."
- MUSE AV โ AI image studio. The orb is the collapsed state of its creation assistant, where image generation routinely takes 30โ90 seconds.
Using it somewhere? Open a PR and add yourself.
MIT ยฉ webkubor