CSS / SVG · 4.2 KB gzip
Spectral Smoke
Soft rising smoke wisps that billow upward — pure atmosphere, no spinner ornaments.
signatureelementalsmokemistatmospheric
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/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
| 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.