All loaders

CSS / SVG · 3.4 KB gzip

Breathing Ring

A glowing ring with a breathing scale cycle and a rotating dashed segment.

ringbreatheminimalcalm
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/breathing-ring';

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

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

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

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

Component source breathing-ring.ts

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

/**
 * Breathing Ring: a soft glowing ring that breathes while a dashed segment
 * slowly rotates around it. Calm and meditative: slow when idle, brighter and
 * faster while streaming, the segment spins up while calling a tool, a solid
 * green ring on `done`, red on `error`.
 */
const STYLES = /* css */ `
:host { --_breath: 3.4s; --_spin: 3s; }
.ring { display: grid; place-items: center; inline-size: 100%; block-size: 100%; }
svg { inline-size: 100%; block-size: 100%; overflow: visible; }
.halo {
  fill: none;
  stroke: var(--orbux-color, #6366f1);
  stroke-width: 7;
  transform-box: fill-box;
  transform-origin: center;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--orbux-color, #6366f1) 55%, transparent));
  animation: orbux-breathe calc(var(--_breath) * var(--orbux-speed-scale)) ease-in-out infinite;
}
.dash {
  fill: none;
  stroke: var(--orbux-color-2, #a855f7);
  stroke-width: 7;
  stroke-linecap: round;
  stroke-dasharray: 22 78;
  transform-box: fill-box;
  transform-origin: center;
  animation: orbux-spin calc(var(--_spin) * var(--orbux-speed-scale)) linear infinite;
}
@keyframes orbux-breathe {
  0%, 100% { transform: scale(0.86); opacity: 0.55; }
  50%      { transform: scale(1.04); opacity: 1; }
}
@keyframes orbux-spin { to { transform: rotate(360deg); } }

/* ---- states ---- */
:host([data-state="idle"])         { --_breath: 6s;   --_spin: 6s; }
:host([data-state="waiting"])      { --_breath: 5s;   --_spin: 5s; }
:host([data-state="thinking"])     { --_breath: 3.4s; --_spin: 3s; }
:host([data-state="streaming"])    { --_breath: 2s;   --_spin: 2s; }
:host([data-state="tool-calling"]) { --_breath: 3s;   --_spin: 1s; }
:host([data-state="idle"]) .halo,
:host([data-state="idle"]) .dash { animation: none; opacity: .48; }
:host([data-state="waiting"]) .halo { animation: none; opacity: .82; }
:host([data-state="waiting"]) .dash { animation: none; opacity: .35; stroke-dashoffset: 28; }

:host([data-state="done"]) .halo { stroke: var(--orbux-color-success, #22c55e); animation: orbux-terminal-bloom 220ms ease-out 1; }
:host([data-state="done"]) .dash { display: none; }

:host([data-state="error"]) { --_breath: 2.4s; }
:host([data-state="error"]) .halo { stroke: var(--orbux-color-error, #ef4444); filter: drop-shadow(0 0 6px color-mix(in srgb, var(--orbux-color-error, #ef4444) 55%, transparent)); animation:orbux-terminal-error 200ms ease-out 1; }
:host([data-state="error"]) .dash { stroke: var(--orbux-color-error, #ef4444); animation:none; }
@keyframes orbux-terminal-bloom { 50% { transform:scale(1.08); } }
@keyframes orbux-terminal-error { 35% { transform:translateX(-4%); } 70% { transform:translateX(4%); } }
:host([data-settled="true"]) .halo { animation:none;transform:none;opacity:.9; }

:host([data-paused="true"]) .halo,
:host([data-paused="true"]) .dash { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .halo,
:host([data-reduced-motion="true"]) .dash { animation: none !important; transform: none; opacity: 0.9; }
:host([data-reduced-motion="true"][data-state="streaming"]) .dash { stroke-dasharray:42 58; }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .dash { stroke-dasharray:12 13; }
:host([data-reduced-motion="true"][data-state="waiting"]) .dash { opacity:.3; }
`;

export class OrbuxBreathingRing extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const ring = document.createElement('div');
    ring.className = 'ring';
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', '0 0 100 100');
    svg.innerHTML =
      '<circle class="halo" part="halo" cx="50" cy="50" r="34"/>' +
      '<circle class="dash" part="dash" cx="50" cy="50" r="34" pathLength="100"/>';
    ring.appendChild(svg);
    this.root.append(style, ring);
  }
}

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

declare global {
  interface HTMLElementTagNameMap {
    'orbux-breathing-ring': OrbuxBreathingRing;
  }
}

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.