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.

StateMeaning
idlePresent but not working.
thinkingReasoning; indeterminate.
streamingEmitting output tokens.
tool-callingExecuting a tool or action.
waitingAwaiting human input.
doneCompleted successfully.
errorFailed.
cancelledStopped 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:

EventFires when
orbux:statechangeAny state change. event.detail = { state, previous }.
orbux:done · orbux:error · orbux:cancelledThe loader reaches that terminal state.
orbux:themerefreshrefreshTheme() 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

<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

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>
Back to the gallery