All loaders

CSS / SVG · 3.3 KB gzip

Arc Spinner

A conic-gradient ring that switches from continuous rotation to a determinate arc when progress is set.

spinnerringgradientprogress
Interactive preview

Usage

Download HTML

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/arc-spinner';

<orbux-arc-spinner state="thinking"></orbux-arc-spinner>

Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:

<script type="module" src="http://localhost:4321/orbux/arc-spinner.js"></script>

<orbux-arc-spinner state="thinking"></orbux-arc-spinner>

Component source arc-spinner.ts

import { OrbuxElement } from '@vikast908/core';

/**
 * Arc Spinner: a rotating conic-gradient ring. Indeterminate by default; if a
 * `progress` value (0..1) is set it becomes a determinate arc filled to that
 * fraction. Turns a full green ring on done, red on error.
 */
const STYLES = /* css */ `
@property --_p {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 0%;
}
:host { --_dur: 1.1s; --_p: 0%; }
.ring {
  inline-size: 100%;
  block-size: 100%;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    transparent 0%,
    color-mix(in srgb, var(--orbux-color, #6366f1) 55%, transparent) 55%,
    var(--orbux-color, #6366f1) 85%,
    var(--orbux-color-2, #a855f7) 100%);
  -webkit-mask: radial-gradient(farthest-side, #0000 76%, #000 78%);
  mask: radial-gradient(farthest-side, #0000 76%, #000 78%);
  animation: orbux-arc-spin calc(var(--_dur) * var(--orbux-speed-scale)) linear infinite;
}

@keyframes orbux-arc-spin { to { transform: rotate(360deg); } }

/* ---- indeterminate state speeds ---- */
:host([data-state="thinking"])     { --_dur: 1.1s; }
:host([data-state="streaming"])    { --_dur: 0.7s; }
:host([data-state="tool-calling"]) { --_dur: 0.9s; }
:host([data-state="tool-calling"]) .ring { border-radius: 28%; animation-timing-function: steps(8, end); }
:host([data-state="idle"])         { --_dur: 2.6s; }
:host([data-state="waiting"])      { --_dur: 2.2s; }
:host([data-state="idle"]) .ring { animation: none; opacity: .48; transform: rotate(28deg); }
:host([data-state="waiting"]) .ring {
  animation: none;
  opacity: .78;
  transform: rotate(-18deg);
  background: conic-gradient(from 0deg,
    transparent 0 38%,
    color-mix(in srgb, var(--orbux-color, #6366f1) 70%, transparent) 38% 72%,
    transparent 72%);
}

/* ---- determinate (progress set) ---- */
:host([data-determinate]) .ring {
  animation: none;
  transition: --_p 120ms ease-out;
  background: conic-gradient(
    var(--orbux-color-progress, var(--orbux-color-2, #a855f7)) var(--_p),
    color-mix(in srgb, var(--orbux-color, #6366f1) 18%, transparent) 0);
}

/* ---- done / error (override determinate) ---- */
:host([data-state="done"]) .ring {
  animation: none;
  background: conic-gradient(var(--orbux-color-success, #22c55e) 100%, transparent 0);
}
:host([data-state="error"]) .ring {
  animation: orbux-arc-error 200ms ease-out 1;
  background: conic-gradient(var(--orbux-color-error, #ef4444) 100%, transparent 0);
}
@keyframes orbux-arc-error { 35% { transform: rotate(-10deg); } 70% { transform: rotate(10deg); } }
:host([data-settled="true"]) .ring { animation: none; }

/* ---- run state ---- */
:host([data-paused="true"]) .ring { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .ring { animation: none !important; }
:host([data-reduced-motion="true"][data-state="thinking"]) .ring { transform:rotate(45deg); }
:host([data-reduced-motion="true"][data-state="streaming"]) .ring { background:conic-gradient(var(--orbux-color,#6366f1) 72%,transparent 0); }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .ring { border-radius:28%; }
:host([data-reduced-motion="true"][data-state="waiting"]) .ring { opacity:.48; }
`;

export class OrbuxArcSpinner extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const ring = document.createElement('div');
    ring.className = 'ring';
    ring.setAttribute('part', 'ring');
    this.root.append(style, ring);
  }

  protected onProgressChange(progress: number | null): void {
    if (progress == null) {
      this.removeAttribute('data-determinate');
    } else {
      this.setAttribute('data-determinate', '');
      this.style.setProperty('--_p', `${(progress * 100).toFixed(1)}%`);
    }
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-arc-spinner')) {
  customElements.define('orbux-arc-spinner', OrbuxArcSpinner);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-arc-spinner': OrbuxArcSpinner;
  }
}

Depends on @vikast908/core. Paste the source, add the base element, and set state from your agent code.

Attributes & properties

NameTypeDescription
stateAgentState idle · thinking · streaming · tool-calling · waiting · done · error
progressnumber (0 to 1) Determinate progress from 0 to 1.
sizeCSS lengthOverall square size.
--orbux-widthCSS lengthOptional inline-size override.
--orbux-heightCSS lengthOptional block-size override.
speednumberAnimation speed multiplier (1 = default).
labelstringAccessible label (defaults per state).
pausedbooleanFreeze 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.