Documentation
Use OrbUX in your interface.
Register a custom element, set its state, and adjust it with attributes or CSS custom properties. Framework wrappers are not required.
Every OrbUX loader is a standard Web Component. It works in React, Vue, Svelte, Angular, or plain HTML with no wrapper, and shares one contract regardless of whether it's drawn with CSS, Canvas, or WebGL. Lottie is a future tier, not a current runtime dependency.
Quick start
Install from npm under the permanent @vikast908 scope
(@vikast908/core and @vikast908/loaders). Product custom-element tags
stay orbux-*. Use 0.2.2 or newer (loaders 0.1.0
shipped with a broken dependency and is deprecated on npm).
npm install @vikast908/loaders@0.2.2 @vikast908/core@0.2.2
import '@vikast908/loaders/pulse-orb';
<orbux-pulse-orb state="thinking" size="140px"></orbux-pulse-orb>
const orb = document.querySelector('orbux-pulse-orb');
orb.state = 'streaming';
orb.progress = 0.6; // determinate loaders only
No npm yet? Use Download HTML on any loader detail page for a self-contained
demo, or clone this monorepo and run pnpm install && pnpm dev.
Standalone HTML
Every loader has a self-contained HTML demo with the browser bundle inlined. On a detail page,
Download HTML saves the file (or opens the tab if save fails).
Copy HTML copies the demo markup and shows ✓ Copied or
Copy failed. No build step. Open the file in a modern browser, or hit a gallery
URL such as
/orbux/pulse-orb.html.
Gallery inspector
Detail pages ship with state chips, size and speed sliders, optional primary and secondary color pickers, and a progress slider when the loader supports it. Progress starts indeterminate; drag to set a value, double-click to turn it off. Home filters by rendering tier. Catalog ANDs use-case and technology. Lab and matrix push one shared state (and lab progress) across many loaders.
The agent state model
The state attribute maps an agent lifecycle to visual behavior. The model is
transport-agnostic and does not depend on an AI SDK.
| State | Meaning |
|---|---|
idle | Present but not working. |
thinking | Reasoning; indeterminate. |
streaming | Emitting output tokens. |
tool-calling | Executing a tool or action. |
waiting | Awaiting human input. |
done | Completed successfully. |
error | Failed. |
cancelled | Stopped by the user (a Stop button or aborted request). Neutral, not a success or failure cue. |
done, error, and cancelled are the three terminal
outcomes. Loaders declare which states they distinguish; unsupported states fall back
gracefully. Use progress (0 to 1) for determinate loaders. Set a custom
label to override the spoken text (for example, "Searching the web…" during
tool-calling).
Driving state from your agent
The model is transport-agnostic, so you can map any SDK onto it. Optional adapters ship as a
separate, tree-shakeable entry point, @vikast908/core/adapters:
import { fromVercelStatus, fromAgUiEvent, fromOpenAiStream, bindAbort } from '@vikast908/core/adapters';
// Vercel AI SDK (useChat / useCompletion 'status')
loader.state = fromVercelStatus(status, { hasMessages: messages.length > 0 });
// AG-UI protocol events (null = transition, no state change)
const next = fromAgUiEvent(event.type);
if (next) loader.state = next;
// OpenAI streaming (Responses events or Chat Completions finish_reason)
const s = fromOpenAiStream({ type: chunk.type, toolCall });
if (s) loader.state = s;
// A "Stop" button: abort -> cancelled
const controller = new AbortController();
bindAbort(loader, controller.signal);
Prefer to wire it yourself? Every state name is just a string, e.g.
loader.state = 'tool-calling'. The adapters only save you the switch statement.
Events
Loaders emit composed, bubbling events so surrounding UI can follow along:
| Event | Fires when |
|---|---|
orbux:statechange | Any state change. event.detail = { state, previous }. |
orbux:done · orbux:error · orbux:cancelled | The loader reaches that terminal state. |
orbux:themerefresh | refreshTheme() ran (or an orbux:themechange was handled). |
orb.addEventListener('orbux:statechange', (e) => {
console.log(e.detail.previous, '→', e.detail.state);
});
orb.addEventListener('orbux:cancelled', () => toast('Stopped')); Announcements, labels, and live activity
- Screen-reader announcements are debounced. Rapid churn
(
thinking → tool-calling → streaming) is coalesced into one announcement so the live region is not spammed mid-turn. Terminal states announce immediately. -
quiet: set thequietattribute (orloader.quiet = true) to stop live announcements entirely when a nearby status message already narrates the agent. The accessible name stays current. -
show-label: set theshow-labelattribute to render a visible caption beneath the loader (the current label, or your customlabel). Style it with::part(label)and--orbux-label-color/--orbux-label-size/--orbux-label-gap. -
pulse(): callloader.pulse()once per streamed token or chunk to make data-driven loaders (Waveform Scope, Wave Bars, …) track real throughput instead of a fixed timer. It exposes a decaying--orbux-activity(0–1) to CSS andinfo.activityto Canvas/WebGL loaders.
<orbux-waveform-scope show-label label="Generating…"></orbux-waveform-scope>
for await (const token of stream) {
scope.pulse(); // swell the trace with real token cadence
append(token);
} Theming
Theme with CSS custom properties. No rebuild is needed:
orbux-pulse-orb {
--orbux-size: 160px;
--orbux-width: 160px; /* optional rectangular override */
--orbux-height: 72px;
--orbux-color: #6366f1;
--orbux-color-2: #a855f7;
--orbux-color-success: #22c55e;
--orbux-color-error: #ef4444;
--orbux-color-cancelled: #94a3b8; /* neutral stop */
--orbux-color-progress: #38bdf8;
--orbux-speed: 1; /* optional; same as the speed attribute */
} Accessibility & performance
- Respects
prefers-reduced-motionwith a minimal or static presentation. - Sets
role="status",aria-busy, and a state-derived label. - Determinate loaders switch to
role="progressbar"with percentage values. - Auto-pauses CSS and rAF motion when scrolled offscreen or the browser tab is hidden (
data-paused). - WebGL loaders clamp device-pixel-ratio and render a single static frame when paused.
If a theme changes on an ancestor, call loader.refreshTheme() or dispatch
orbux:themechange on window. Import
@vikast908/loaders/preflight.css to reserve loader dimensions before custom elements
upgrade.
Framework examples
OrbUX loaders are standard custom elements, so every framework can use them without a wrapper.
Register the one you need with a side-effect import, then bind state like any
attribute.
// React (19+ passes props to custom elements directly)
import '@vikast908/loaders/pulse-orb';
export function Thinking({ status }) {
// In React ≤18, set string attributes: state={String(status)}
return <orbux-pulse-orb state={status} size="120px" />;
} <!-- Vue: tell the compiler orbux-* are custom elements
(vite.config: vue({ template: { compilerOptions: {
isCustomElement: (t) => t.startsWith('orbux-') } } })) -->
<script setup>
import '@vikast908/loaders/pulse-dots';
</script>
<template>
<orbux-pulse-dots :state="status" />
</template> <!-- Svelte binds attributes to custom elements natively -->
<script>
import '@vikast908/loaders/wave-bars';
export let status;
</script>
<orbux-wave-bars state={status} /> Server rendering & upgrade timing
Custom elements only run in the browser, so on the server a loader renders as an empty box
until its script loads and upgrades it. Import
@vikast908/loaders/preflight.css once so the element reserves its size and does
not shift the layout before upgrade:
// Next.js: register in a Client Component 'use client'; import '@vikast908/loaders/pulse-orb'; import '@vikast908/loaders/preflight.css';
No build step (CDN)
Load a single loader straight from a CDN as an ES module, with no bundler or install step:
<script type="module"> import 'https://esm.sh/@vikast908/loaders@0.2.2/pulse-orb'; </script> <orbux-pulse-orb state="thinking" size="140px"></orbux-pulse-orb>