All loaders

CSS / SVG · 3.5 KB gzip

Frost Bloom

A crystalline snow bloom with branching ice arms and a faceted pulsing core.

signatureelementalicecrystalsnowflake
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/frost-bloom';

<orbux-frost-bloom state="thinking"></orbux-frost-bloom>

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

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

<orbux-frost-bloom state="thinking"></orbux-frost-bloom>

Component source frost-bloom.ts

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

const STYLES = /* css */ `
:host { --_tone:var(--orbux-color,#67e8f9);--_tone-2:var(--orbux-color-2,#e0f2fe);--_dur:2.8s; }
.frost { position:relative;inline-size:100%;block-size:100%;display:grid;place-items:center;filter:drop-shadow(0 0 7px color-mix(in srgb,var(--_tone) 58%,transparent));animation:orbux-frost-turn calc(var(--_dur) * var(--orbux-speed-scale)) linear infinite; }
.arm { position:absolute;left:48%;bottom:50%;inline-size:4%;block-size:43%;border-radius:999px;background:linear-gradient(to top,var(--_tone),var(--_tone-2));transform-origin:50% 100%;rotate:var(--_angle); }
.arm::before,.arm::after { content:"";position:absolute;top:42%;inline-size:420%;block-size:3px;border-radius:999px;background:var(--_tone);transform-origin:left center; }
.arm::before { left:50%;transform:rotate(42deg); }.arm::after { right:50%;transform:rotate(138deg);transform-origin:right center; }
.crystal { position:absolute;inset:35%;clip-path:polygon(50% 0,88% 22%,100% 66%,50% 100%,0 66%,12% 22%);background:linear-gradient(135deg,var(--_tone-2),var(--_tone) 56%,color-mix(in srgb,var(--_tone) 58%,#0f172a));box-shadow:inset 0 0 0 2px #ffffff55;animation:orbux-frost-pulse calc(1.4s * var(--orbux-speed-scale)) cubic-bezier(.77,0,.175,1) infinite; }
@keyframes orbux-frost-turn { to { transform:rotate(360deg); } }
@keyframes orbux-frost-pulse { 50% { transform:scale(.82) rotate(30deg); } }
@keyframes orbux-frost-done { 50% { transform:scale(1.055); } }
@keyframes orbux-frost-error { 35% { transform:translateX(-4%); } 70% { transform:translateX(4%); } }
:host([data-state="streaming"]) { --_dur:1.4s; }
:host([data-state="tool-calling"]) .frost { animation-timing-function:steps(8,end); }
:host([data-state="tool-calling"]) .crystal { clip-path:polygon(50% 8%,92% 28%,92% 72%,50% 92%,8% 72%,8% 28%); transform:scale(.9); }
:host([data-state="idle"]) .frost,:host([data-state="waiting"]) .frost,:host([data-state="idle"]) .crystal,:host([data-state="waiting"]) .crystal { animation:none; }
:host([data-state="idle"]) .frost { opacity:.48; }
:host([data-state="waiting"]) .frost { opacity:.82; }
:host([data-state="waiting"]) .crystal { transform:scale(.82); }
:host([data-state="waiting"]) .arm:nth-child(even) { opacity:.28; }
:host([data-state="done"]) { --_tone:var(--orbux-color-success,#22c55e);--_tone-2:#dcfce7; }
:host([data-state="error"]) { --_tone:var(--orbux-color-error,#ef4444);--_tone-2:#fee2e2; }
:host([data-state="done"]) .frost,:host([data-state="error"]) .frost,:host([data-state="done"]) .crystal,:host([data-state="error"]) .crystal { animation:none; }
:host([data-state="done"]) .frost { animation:orbux-frost-done 220ms cubic-bezier(.23,1,.32,1) 1; }
:host([data-state="error"]) .frost { animation:orbux-frost-error 200ms cubic-bezier(.23,1,.32,1) 1; }
:host([data-settled="true"]) .frost { 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"]) .crystal { transform:rotate(30deg); }
`;

export class OrbuxFrostBloom extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const frost = document.createElement('div');
    frost.className = 'frost';
    frost.setAttribute('part', 'frost');
    frost.innerHTML = `${Array.from({ length: 6 }, (_, index) => `<i class="arm" style="--_angle:${index * 60}deg"></i>`).join('')}<span class="crystal"></span>`;
    this.root.append(style, frost);
  }
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-frost-bloom'))
  customElements.define('orbux-frost-bloom', OrbuxFrostBloom);
declare global {
  interface HTMLElementTagNameMap {
    'orbux-frost-bloom': OrbuxFrostBloom;
  }
}

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.