All loaders

CSS / SVG · 3.5 KB gzip

Orbit Ring

A thin ring with a satellite dot orbiting a glowing core: mechanical and precise, it tightens while calling a tool.

ringorbitsatellitemechanicaldependency-free
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/orbit-ring';

<orbux-orbit-ring state="thinking"></orbux-orbit-ring>

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

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

<orbux-orbit-ring state="thinking"></orbux-orbit-ring>

Component source orbit-ring.ts

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

/**
 * Orbit Ring: a thin ring with a satellite dot orbiting a glowing core.
 * Mechanical, precise. Speeds up and tightens while calling a tool, leaves a
 * faint comet arc while streaming, parks and turns green on done.
 */
const STYLES = /* css */ `
:host { --_dur: 2.4s; --_inset: 9%; }
.stage {
  position: relative;
  inline-size: 100%;
  block-size: 100%;
}
.ring {
  position: absolute;
  inset: var(--_inset);
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--orbux-color, #6366f1) 28%, transparent);
}
.core {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 50%;
  inline-size: 15%;
  aspect-ratio: 1;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle at 30% 30%, var(--orbux-color-2, #a855f7), var(--orbux-color, #6366f1));
  box-shadow: 0 0 8px color-mix(in srgb, var(--orbux-color, #6366f1) 60%, transparent);
}
.orbit {
  position: absolute;
  inset: var(--_inset);
  animation: orbux-orbit-spin calc(var(--_dur) * var(--orbux-speed-scale)) linear infinite;
}
.sat {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  inline-size: 17%;
  aspect-ratio: 1;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: var(--orbux-color-2, #a855f7);
  box-shadow: 0 0 8px var(--orbux-color-2, #a855f7);
}
.trail {
  position: absolute;
  inset: var(--_inset);
  border-radius: 50%;
  opacity: 0;
  animation: orbux-orbit-spin calc(var(--_dur) * var(--orbux-speed-scale)) linear infinite;
  background: conic-gradient(from -20deg,
    transparent 0 68%,
    color-mix(in srgb, var(--orbux-color-2, #a855f7) 55%, transparent) 92%,
    transparent 100%);
  -webkit-mask: radial-gradient(farthest-side, #0000 82%, #000 84%);
  mask: radial-gradient(farthest-side, #0000 82%, #000 84%);
}

@keyframes orbux-orbit-spin { to { transform: rotate(360deg); } }
@keyframes orbux-orbit-jitter {
  0%, 100% { transform: translate(0, 0); }
  25% { transform: translate(-4%, 2%); }
  75% { transform: translate(4%, -2%); }
}

/* ---- states ---- */
:host([data-state="thinking"])     { --_dur: 2.4s; }
:host([data-state="streaming"])    { --_dur: 1.5s; }
:host([data-state="streaming"]) .trail { opacity: 1; }
:host([data-state="tool-calling"]) { --_dur: 0.85s; --_inset: 20%; }
:host([data-state="idle"])         { --_dur: 4.5s; }
:host([data-state="waiting"])      { --_dur: 3.6s; }
:host([data-state="idle"]) .orbit,
:host([data-state="idle"]) .trail,
:host([data-state="waiting"]) .orbit,
:host([data-state="waiting"]) .trail { animation: none; }
:host([data-state="idle"]) .stage,
:host([data-state="waiting"]) .stage { opacity: 0.7; }

:host([data-state="done"]) .orbit { animation: none; transform: rotate(0deg); }
:host([data-state="done"]) .trail { opacity: 0; }
:host([data-state="done"]) .ring { border-color: color-mix(in srgb, var(--orbux-color-success, #22c55e) 70%, transparent); }
:host([data-state="done"]) .sat,
:host([data-state="done"]) .core {
  background: var(--orbux-color-success, #22c55e);
  box-shadow: 0 0 10px var(--orbux-color-success, #22c55e);
}

:host([data-state="error"]) .orbit { animation: none; }
:host([data-state="error"]) .trail { opacity: 0; }
:host([data-state="error"]) .stage { animation: orbux-orbit-jitter 200ms ease-out 1; }
:host([data-state="error"]) .ring { border-color: color-mix(in srgb, var(--orbux-color-error, #ef4444) 65%, transparent); }
:host([data-state="error"]) .sat,
:host([data-state="error"]) .core {
  background: var(--orbux-color-error, #ef4444);
  box-shadow: 0 0 10px var(--orbux-color-error, #ef4444);
}
:host([data-settled="true"]) .stage { animation:none; }

/* ---- run state ---- */
:host([data-paused="true"]) .orbit,
:host([data-paused="true"]) .trail,
:host([data-paused="true"]) .stage { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .orbit,
:host([data-reduced-motion="true"]) .trail,
:host([data-reduced-motion="true"]) .stage { animation: none !important; }
:host([data-reduced-motion="true"][data-state="streaming"]) .trail { display:block;opacity:1; }
:host([data-reduced-motion="true"][data-state="tool-calling"]) { --_inset:20%; }
:host([data-reduced-motion="true"][data-state="waiting"]) .sat { opacity:.35; }
`;

export class OrbuxOrbitRing extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const stage = document.createElement('div');
    stage.className = 'stage';
    stage.setAttribute('part', 'stage');
    stage.innerHTML =
      '<div class="ring" part="ring"></div>' +
      '<div class="trail" part="trail"></div>' +
      '<div class="core" part="core"></div>' +
      '<div class="orbit"><span class="sat" part="satellite"></span></div>';
    this.root.append(style, stage);
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-orbit-ring')) {
  customElements.define('orbux-orbit-ring', OrbuxOrbitRing);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-orbit-ring': OrbuxOrbitRing;
  }
}

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
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.