CSS / SVG · 3.1 KB gzip
Button Spinner
A compact current-color spinner for buttons and inline actions, with terminal check and error marks.
spinnerbuttoninlinecurrentColor
Interactive preview
Usage
Packages are @vikast908/core and
@vikast908/loaders. Prefer 0.1.1+
(0.1.0 loaders is broken on npm).
npm install @vikast908/loaders@0.1.1 @vikast908/core@0.1.1
import '@vikast908/loaders/button-spinner'; <orbux-button-spinner state="thinking"></orbux-button-spinner>
Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:
<script type="module" src="http://localhost:4321/orbux/button-spinner.js"></script> <orbux-button-spinner state="thinking"></orbux-button-spinner>
Component source button-spinner.ts
import { OrbuxElement } from '@vikast908/core';
const STYLES = /* css */ `
:host{inline-size:var(--orbux-width,var(--orbux-size,20px));block-size:var(--orbux-height,var(--orbux-size,20px));color:var(--orbux-color,currentColor)}
.spinner{position:relative;inline-size:100%;block-size:100%;border:2px solid color-mix(in srgb,currentColor 22%,transparent);border-block-start-color:currentColor;border-radius:50%;animation:orbux-button-spin calc(.72s * var(--orbux-speed-scale)) linear infinite}
@keyframes orbux-button-spin{to{transform:rotate(1turn)}}
:host([data-state="idle"]) .spinner{animation:none;opacity:.42}
:host([data-state="waiting"]) .spinner{animation:none;opacity:.72;border-style:dashed}
:host([data-state="streaming"]) .spinner{animation-duration:calc(.48s * var(--orbux-speed-scale))}
:host([data-state="tool-calling"]) .spinner{border-radius:28%;animation-timing-function:steps(4,end)}
:host([data-state="done"]) .spinner,:host([data-state="error"]) .spinner{border:0;animation:none}
:host([data-state="done"]) .spinner::after{content:"";position:absolute;inset:18% 8% 28% 18%;border-inline-end:2px solid var(--orbux-color-success,#22c55e);border-block-end:2px solid var(--orbux-color-success,#22c55e);transform:rotate(42deg);animation:orbux-button-pop 200ms ease-out 1}
:host([data-state="error"]) .spinner::before,:host([data-state="error"]) .spinner::after{content:"";position:absolute;inset-inline-start:48%;inset-block:10%;inline-size:2px;background:var(--orbux-color-error,#ef4444);border-radius:2px;animation:orbux-button-pop 180ms ease-out 1}
:host([data-state="error"]) .spinner::before{transform:rotate(45deg)}
:host([data-state="error"]) .spinner::after{transform:rotate(-45deg)}
@keyframes orbux-button-pop{from{opacity:0;scale:.72}}
:host([data-settled="true"]) .spinner::before,:host([data-settled="true"]) .spinner::after{animation:none}
:host([data-paused="true"]) .spinner{animation-play-state:paused}
:host([data-reduced-motion="true"]) .spinner{animation:none}
:host([data-reduced-motion="true"][data-state="streaming"]) .spinner{border-width:3px}
:host([data-reduced-motion="true"][data-state="thinking"]) .spinner{opacity:.78}
`;
export class OrbuxButtonSpinner extends OrbuxElement {
protected build(): void {
const style = document.createElement('style');
style.textContent = STYLES;
const spinner = document.createElement('span');
spinner.className = 'spinner';
spinner.setAttribute('part', 'spinner');
this.root.append(style, spinner);
}
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-button-spinner')) {
customElements.define('orbux-button-spinner', OrbuxButtonSpinner);
}
declare global {
interface HTMLElementTagNameMap {
'orbux-button-spinner': OrbuxButtonSpinner;
}
}
Depends on @vikast908/core. Paste the source, add the base
element, and set state from your agent code.
Attributes & properties
| Name | Type | Description |
|---|---|---|
state | AgentState | idle · thinking · streaming · tool-calling · waiting · done · error |
size | CSS length | Overall square size. |
--orbux-width | CSS length | Optional inline-size override. |
--orbux-height | CSS length | Optional block-size override. |
speed | number | Animation speed multiplier (1 = default). |
label | string | Accessible label (defaults per state). |
paused | boolean | Freeze the animation. |
Respects prefers-reduced-motion, exposes
status/progressbar semantics with a live label, and auto-pauses (sets
data-paused) when offscreen or the tab is hidden.