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