All loaders

CSS / SVG · 4.2 KB gzip

Spectral Smoke

Soft rising smoke wisps that billow upward — pure atmosphere, no spinner ornaments.

signatureelementalsmokemistatmospheric
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/spectral-smoke';

<orbux-spectral-smoke state="thinking"></orbux-spectral-smoke>

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

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

<orbux-spectral-smoke state="thinking"></orbux-spectral-smoke>

Component source spectral-smoke.ts

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

/**
 * Spectral Smoke: soft rising wisps only — no rings or spinner ornaments.
 * Layered translucent plumes that billow and drift upward.
 */
const STYLES = /* css */ `
:host {
  --_tone: var(--orbux-color, #94a3b8);
  --_tone-2: var(--orbux-color-2, #c4b5fd);
  --_dur: 3.2s;
  --_ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --_ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}

.smoke {
  position: relative;
  inline-size: 100%;
  block-size: 100%;
  overflow: hidden;
}

/* Soft source haze at the bottom — not a hard shape */
.source {
  position: absolute;
  inset-inline: 18%;
  inset-block-end: 0;
  block-size: 22%;
  border-radius: 50%;
  background: radial-gradient(
    ellipse at 50% 80%,
    color-mix(in srgb, var(--_tone) 42%, transparent) 0%,
    color-mix(in srgb, var(--_tone-2) 18%, transparent) 48%,
    transparent 72%
  );
  filter: blur(3px);
  opacity: 0.7;
  animation: orbux-smoke-source calc(2.4s * var(--orbux-speed-scale)) var(--_ease-in-out) infinite;
}

/* Elongated soft plumes — smoke columns, never circular loaders */
.wisp {
  position: absolute;
  inset-inline-start: var(--_x);
  inset-block-end: -8%;
  inline-size: var(--_w);
  block-size: var(--_h);
  border-radius: 60% 40% 55% 45% / 35% 45% 55% 65%;
  background: radial-gradient(
    ellipse 70% 55% at 50% 70%,
    color-mix(in srgb, var(--_tone) 55%, white) 0%,
    color-mix(in srgb, var(--_tone-2) 40%, var(--_tone)) 28%,
    color-mix(in srgb, var(--_tone) 22%, transparent) 58%,
    transparent 78%
  );
  filter: blur(var(--_blur, 2.2px));
  opacity: 0;
  transform-origin: 50% 100%;
  animation: orbux-smoke-rise calc((var(--_dur) + var(--_i) * 0.22s) * var(--orbux-speed-scale))
    var(--_ease-out) infinite;
  animation-delay: calc(var(--_i) * -0.48s * var(--orbux-speed-scale));
  will-change: transform, opacity;
}

/* Soft side drift layers for depth */
.wisp.is-soft {
  filter: blur(3.4px);
  opacity: 0;
}

@keyframes orbux-smoke-rise {
  0% {
    transform: translate3d(0, 8%, 0) scale(0.55, 0.7) rotate(var(--_rot-start, -4deg));
    opacity: 0;
  }
  12% {
    opacity: var(--_peak, 0.55);
  }
  55% {
    opacity: calc(var(--_peak, 0.55) * 0.72);
  }
  100% {
    transform: translate3d(var(--_drift), -118%, 0) scale(1.15, 1.35) rotate(var(--_rot-end, 8deg));
    opacity: 0;
  }
}

@keyframes orbux-smoke-source {
  0%, 100% { transform: scaleX(0.92); opacity: 0.55; }
  50% { transform: scaleX(1.08); opacity: 0.85; }
}

@keyframes orbux-smoke-done {
  0% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.04); opacity: 0.9; }
  100% { transform: scale(1); opacity: 1; }
}

@keyframes orbux-smoke-error {
  0%, 100% { transform: translateX(0); }
  30% { transform: translateX(-2.5%); }
  60% { transform: translateX(2.5%); }
}

/* ---- states ---- */
:host([data-state="thinking"]) { --_dur: 3.2s; }
:host([data-state="streaming"]) { --_dur: 1.7s; }
:host([data-state="streaming"]) .wisp { --_peak: 0.72; }
:host([data-state="streaming"]) .source { opacity: 0.95; }

:host([data-state="tool-calling"]) { --_dur: 2.2s; }
:host([data-state="tool-calling"]) .wisp {
  animation-timing-function: var(--_ease-in-out);
}
:host([data-state="tool-calling"]) .source {
  filter: blur(2px);
  opacity: 0.9;
}

:host([data-state="idle"]) .wisp,
:host([data-state="idle"]) .source { animation: none; }
:host([data-state="idle"]) .smoke { opacity: 0.42; }
:host([data-state="idle"]) .wisp {
  opacity: 0.18;
  transform: translate3d(0, -28%, 0) scale(0.85, 0.9);
}
:host([data-state="idle"]) .source {
  opacity: 0.3;
  transform: scaleX(0.9);
}

:host([data-state="waiting"]) .wisp,
:host([data-state="waiting"]) .source { animation: none; }
:host([data-state="waiting"]) .smoke { opacity: 0.78; }
:host([data-state="waiting"]) .wisp {
  opacity: 0.32;
  transform: translate3d(0, -42%, 0) scale(0.92, 1);
}
:host([data-state="waiting"]) .wisp:nth-of-type(odd) { opacity: 0.2; }
:host([data-state="waiting"]) .source { opacity: 0.5; }

:host([data-state="done"]) {
  --_tone: var(--orbux-color-success, #22c55e);
  --_tone-2: #bbf7d0;
}
:host([data-state="error"]) {
  --_tone: var(--orbux-color-error, #ef4444);
  --_tone-2: #fecaca;
}
:host([data-state="done"]) .wisp,
:host([data-state="error"]) .wisp,
:host([data-state="done"]) .source,
:host([data-state="error"]) .source { animation: none; }
:host([data-state="done"]) .wisp,
:host([data-state="error"]) .wisp {
  opacity: 0.35;
  transform: translate3d(0, -50%, 0) scale(1, 1.1);
}
:host([data-state="done"]) .smoke { animation: orbux-smoke-done 220ms var(--_ease-out) 1; }
:host([data-state="error"]) .smoke { animation: orbux-smoke-error 200ms var(--_ease-out) 1; }
:host([data-settled="true"]) .smoke { animation: none; }

:host([data-paused="true"]) .wisp,
:host([data-paused="true"]) .source { animation-play-state: paused; }

:host([data-reduced-motion="true"]) .wisp,
:host([data-reduced-motion="true"]) .source { animation: none !important; }
:host([data-reduced-motion="true"]) .wisp {
  opacity: 0.28;
  transform: translate3d(0, calc(var(--_i) * -9% - 20%), 0) scale(0.9, 1);
}
:host([data-reduced-motion="true"]) .source { opacity: 0.45; }
:host([data-reduced-motion="true"][data-state="streaming"]) .wisp { opacity: 0.42; }
:host([data-reduced-motion="true"][data-state="waiting"]) .wisp { opacity: 0.22; }
:host([data-reduced-motion="true"][data-state="idle"]) .smoke { opacity: 0.4; }
`;

export class OrbuxSpectralSmoke extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const smoke = document.createElement('div');
    smoke.className = 'smoke';
    smoke.setAttribute('part', 'smoke');

    // Tall soft columns at staggered positions — pure smoke, no rings.
    const wisps = [
      {
        i: 0,
        x: '28%',
        w: '34%',
        h: '78%',
        drift: '10%',
        rotS: '-6deg',
        rotE: '10deg',
        peak: '0.5',
        blur: '2.4px',
      },
      {
        i: 1,
        x: '42%',
        w: '28%',
        h: '72%',
        drift: '-14%',
        rotS: '4deg',
        rotE: '-8deg',
        peak: '0.48',
        blur: '2px',
        soft: true,
      },
      {
        i: 2,
        x: '18%',
        w: '30%',
        h: '68%',
        drift: '18%',
        rotS: '-3deg',
        rotE: '12deg',
        peak: '0.42',
        blur: '2.8px',
      },
      {
        i: 3,
        x: '48%',
        w: '26%',
        h: '74%',
        drift: '-8%',
        rotS: '5deg',
        rotE: '-6deg',
        peak: '0.46',
        blur: '2.1px',
      },
      {
        i: 4,
        x: '34%',
        w: '36%',
        h: '64%',
        drift: '6%',
        rotS: '-8deg',
        rotE: '5deg',
        peak: '0.38',
        blur: '3.2px',
        soft: true,
      },
      {
        i: 5,
        x: '22%',
        w: '24%',
        h: '70%',
        drift: '16%',
        rotS: '2deg',
        rotE: '-10deg',
        peak: '0.4',
        blur: '2.5px',
      },
    ];

    smoke.innerHTML =
      '<span class="source" part="source"></span>' +
      wisps
        .map((w) => {
          const soft = w.soft ? ' is-soft' : '';
          return (
            `<i class="wisp${soft}" part="wisp" style="` +
            `--_i:${w.i};--_x:${w.x};--_w:${w.w};--_h:${w.h};` +
            `--_drift:${w.drift};--_rot-start:${w.rotS};--_rot-end:${w.rotE};` +
            `--_peak:${w.peak};--_blur:${w.blur}` +
            `"></i>`
          );
        })
        .join('');

    this.root.append(style, smoke);
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-spectral-smoke')) {
  customElements.define('orbux-spectral-smoke', OrbuxSpectralSmoke);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-spectral-smoke': OrbuxSpectralSmoke;
  }
}

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.