All loaders

CSS / SVG · 3.6 KB gzip

Tidal Orb

A glassy water sphere with layered rotating currents and buoyant bubbles.

signatureelementalwaterliquidorb
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/tidal-orb';

<orbux-tidal-orb state="thinking"></orbux-tidal-orb>

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

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

<orbux-tidal-orb state="thinking"></orbux-tidal-orb>

Component source tidal-orb.ts

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

const STYLES = /* css */ `
:host { --_tone:var(--orbux-color,#0ea5e9);--_tone-2:var(--orbux-color-2,#67e8f9);--_dur:2.4s; }
.tidal { position:relative;inline-size:100%;block-size:100%;display:grid;place-items:center;filter:drop-shadow(0 0 9px color-mix(in srgb,var(--_tone) 52%,transparent)); }
.orb { position:absolute;inset:8%;border-radius:50%;overflow:hidden;background:radial-gradient(circle at 36% 28%,#ffffff9a 0 4%,transparent 5%),linear-gradient(155deg,color-mix(in srgb,var(--_tone-2) 65%,white),var(--_tone) 52%,color-mix(in srgb,var(--_tone) 58%,#082f49));box-shadow:inset -8px -10px 14px #082f4988,inset 4px 5px 8px #ffffff55; }
.wave { position:absolute;left:-35%;bottom:calc(-54% + var(--_i) * 17%);inline-size:170%;aspect-ratio:1.9;border-radius:43% 57% 46% 54%;background:color-mix(in srgb,var(--_color) 58%,transparent);transform-origin:center;animation:orbux-tidal-roll calc((var(--_dur) + var(--_i) * .35s) * var(--orbux-speed-scale)) linear infinite;animation-direction:var(--_direction); }
.bubble { position:absolute;left:calc(19% + var(--_x));bottom:12%;inline-size:calc(4% + var(--_i) * 1%);aspect-ratio:1;border-radius:50%;border:1px solid #ffffffaa;animation:orbux-tidal-bubble calc((1.5s + var(--_i) * .22s) * var(--orbux-speed-scale)) cubic-bezier(.23,1,.32,1) infinite;animation-delay:calc(var(--_i) * -310ms); }
@keyframes orbux-tidal-roll { to { transform:rotate(360deg); } }
@keyframes orbux-tidal-bubble { 0% { transform:translateY(0) scale(.72);opacity:0; } 20% { opacity:.82; } 100% { transform:translateY(-570%) translateX(35%) scale(1.05);opacity:0; } }
@keyframes orbux-tidal-done { 50% { transform:scale(1.055); } }
@keyframes orbux-tidal-error { 35% { transform:translateX(-4%); } 70% { transform:translateX(4%); } }
:host([data-state="streaming"]) { --_dur:1.2s; }
:host([data-state="tool-calling"]) .wave { animation-timing-function:steps(8,end); }
:host([data-state="idle"]) .wave,:host([data-state="waiting"]) .wave,:host([data-state="idle"]) .bubble,:host([data-state="waiting"]) .bubble { animation:none; }
:host([data-state="idle"]) .tidal { opacity:.48; }
:host([data-state="waiting"]) .tidal { opacity:.82; }
:host([data-state="waiting"]) .wave { transform:rotate(18deg); }
:host([data-state="waiting"]) .bubble:nth-child(even) { opacity:.25; }
:host([data-state="done"]) { --_tone:var(--orbux-color-success,#22c55e);--_tone-2:#86efac; }
:host([data-state="error"]) { --_tone:var(--orbux-color-error,#ef4444);--_tone-2:#fb7185; }
:host([data-state="done"]) .wave,:host([data-state="error"]) .wave,:host([data-state="done"]) .bubble,:host([data-state="error"]) .bubble { animation:none; }
:host([data-state="done"]) .tidal { animation:orbux-tidal-done 220ms cubic-bezier(.23,1,.32,1) 1; }
:host([data-state="error"]) .tidal { animation:orbux-tidal-error 200ms cubic-bezier(.23,1,.32,1) 1; }
:host([data-settled="true"]) .tidal { animation:none; }
:host([data-paused="true"]) * { animation-play-state:paused !important; }
:host([data-reduced-motion="true"]) * { animation:none !important; }
:host([data-reduced-motion="true"][data-state="streaming"]) .wave:nth-child(2) { transform:rotate(28deg); }
`;

export class OrbuxTidalOrb extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const tidal = document.createElement('div');
    tidal.className = 'tidal';
    tidal.setAttribute('part', 'tidal');
    tidal.innerHTML = `<span class="orb"><i class="wave" style="--_i:0;--_color:var(--_tone-2);--_direction:normal"></i><i class="wave" style="--_i:1;--_color:var(--_tone);--_direction:reverse"></i><i class="wave" style="--_i:2;--_color:#082f49;--_direction:normal"></i>${[2, 38, 17, 51].map((x, index) => `<b class="bubble" style="--_i:${index};--_x:${x}%"></b>`).join('')}</span>`;
    this.root.append(style, tidal);
  }
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-tidal-orb'))
  customElements.define('orbux-tidal-orb', OrbuxTidalOrb);
declare global {
  interface HTMLElementTagNameMap {
    'orbux-tidal-orb': OrbuxTidalOrb;
  }
}

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.