CSS / SVG · 4.7 KB gzip
Ember Flame
Layered fire with a hot core, soft outer glow, and rising sparks.
signatureelementalfireflameembers
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/ember-flame'; <orbux-ember-flame state="thinking"></orbux-ember-flame>
Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:
<script type="module" src="http://localhost:4321/orbux/ember-flame.js"></script> <orbux-ember-flame state="thinking"></orbux-ember-flame>
Component source ember-flame.ts
import { OrbuxElement } from '@vikast908/core';
/**
* Ember Flame: layered fire with a hot core, mid and outer tongues, soft glow,
* and rising sparks. Organic sway — not cardboard clip-path cutouts.
*/
const STYLES = /* css */ `
:host {
--_tone: var(--orbux-color, #f97316);
--_tone-2: var(--orbux-color-2, #facc15);
--_hot: color-mix(in srgb, var(--_tone-2) 55%, white);
--_dur: 0.95s;
--_ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--_ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}
.flame {
position: relative;
inline-size: 100%;
block-size: 100%;
display: grid;
place-items: end center;
overflow: visible;
}
/* Soft ground heat — not a hard drop-shadow plate */
.glow {
position: absolute;
inset-inline: 12%;
inset-block-end: 2%;
block-size: 28%;
border-radius: 50%;
background: radial-gradient(
ellipse at center,
color-mix(in srgb, var(--_tone) 55%, transparent) 0%,
color-mix(in srgb, var(--_tone-2) 22%, transparent) 45%,
transparent 72%
);
opacity: 0.85;
animation: orbux-ember-glow calc(1.1s * var(--orbux-speed-scale)) var(--_ease-in-out) infinite;
}
/* Shared teardrop volume: pointed top, wide base */
.tongue {
position: absolute;
inset-inline-start: 50%;
inset-block-end: 10%;
transform: translateX(-50%);
transform-origin: 50% 100%;
border-radius: 50% 50% 42% 42% / 68% 68% 28% 28%;
will-change: transform, opacity;
}
/* Cool outer envelope — soft, slightly blurred edge */
.outer {
inline-size: 52%;
block-size: 72%;
background: radial-gradient(
ellipse 70% 90% at 50% 78%,
color-mix(in srgb, var(--_tone) 88%, transparent) 0 38%,
color-mix(in srgb, var(--_tone) 45%, transparent) 62%,
transparent 78%
);
filter: blur(1.2px);
opacity: 0.92;
animation: orbux-ember-sway-a calc(var(--_dur) * 1.15 * var(--orbux-speed-scale)) var(--_ease-in-out) infinite;
}
/* Mid body — hotter, tighter */
.mid {
inline-size: 38%;
block-size: 62%;
background: radial-gradient(
ellipse 65% 85% at 48% 72%,
var(--_tone-2) 0 22%,
var(--_tone) 48%,
color-mix(in srgb, var(--_tone) 35%, transparent) 72%,
transparent 86%
);
filter: blur(0.4px);
opacity: 0.95;
animation: orbux-ember-sway-b calc(var(--_dur) * var(--orbux-speed-scale)) var(--_ease-in-out) infinite;
animation-delay: calc(-0.12s * var(--orbux-speed-scale));
mix-blend-mode: screen;
}
/* Hot core — white-yellow heart of the fire */
.core {
inline-size: 24%;
block-size: 48%;
background: radial-gradient(
ellipse 60% 80% at 50% 70%,
white 0 12%,
var(--_hot) 28%,
var(--_tone-2) 55%,
color-mix(in srgb, var(--_tone) 40%, transparent) 78%,
transparent 92%
);
opacity: 0.98;
animation: orbux-ember-sway-c calc(var(--_dur) * 0.85 * var(--orbux-speed-scale)) var(--_ease-in-out) infinite;
animation-delay: calc(-0.22s * var(--orbux-speed-scale));
mix-blend-mode: screen;
}
/* Small tip lick above the core */
.tip {
inline-size: 14%;
block-size: 28%;
inset-block-end: 48%;
background: radial-gradient(
ellipse at 50% 80%,
color-mix(in srgb, var(--_hot) 80%, white) 0 30%,
color-mix(in srgb, var(--_tone-2) 55%, transparent) 60%,
transparent 85%
);
filter: blur(0.6px);
opacity: 0.9;
animation: orbux-ember-tip calc(var(--_dur) * 0.7 * var(--orbux-speed-scale)) var(--_ease-in-out) infinite;
mix-blend-mode: screen;
}
/* Rising sparks */
.spark {
position: absolute;
inset-inline-start: calc(32% + var(--_x));
inset-block-end: 22%;
inline-size: calc(3.5% + var(--_s, 0%));
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, white, var(--_tone-2) 45%, transparent 75%);
opacity: 0;
animation: orbux-ember-lift calc((1.15s + var(--_i) * 0.07s) * var(--orbux-speed-scale)) var(--_ease-out) infinite;
animation-delay: calc(var(--_i) * -0.18s * var(--orbux-speed-scale));
pointer-events: none;
}
@keyframes orbux-ember-sway-a {
0%, 100% { transform: translateX(-50%) scale(0.94, 1) rotate(-4deg); opacity: 0.78; }
40% { transform: translateX(-50%) scale(1.04, 0.96) rotate(5deg); opacity: 1; }
70% { transform: translateX(-50%) scale(0.97, 1.02) rotate(-2deg); opacity: 0.9; }
}
@keyframes orbux-ember-sway-b {
0%, 100% { transform: translateX(-50%) scale(0.96, 1.02) rotate(3deg); }
50% { transform: translateX(-50%) scale(1.05, 0.94) rotate(-5deg); }
}
@keyframes orbux-ember-sway-c {
0%, 100% { transform: translateX(-50%) scale(0.92, 1.04) rotate(-2deg); opacity: 0.88; }
50% { transform: translateX(-50%) scale(1.06, 0.92) rotate(3deg); opacity: 1; }
}
@keyframes orbux-ember-tip {
0%, 100% { transform: translateX(-50%) scale(0.85, 1) rotate(-8deg); opacity: 0.55; }
50% { transform: translateX(-50%) scale(1.1, 0.88) rotate(10deg); opacity: 1; }
}
@keyframes orbux-ember-glow {
0%, 100% { transform: scaleX(0.92); opacity: 0.65; }
50% { transform: scaleX(1.06); opacity: 0.95; }
}
@keyframes orbux-ember-lift {
0% { transform: translateY(0) translateX(0) scale(0.85); opacity: 0; }
12% { opacity: 0.95; }
100% { transform: translateY(-520%) translateX(var(--_drift, 40%)) scale(0.35); opacity: 0; }
}
@keyframes orbux-ember-done {
0% { transform: scale(1); }
50% { transform: scale(1.06); }
100% { transform: scale(1); }
}
@keyframes orbux-ember-error {
0%, 100% { transform: translateX(0); }
30% { transform: translateX(-3%); }
60% { transform: translateX(3%); }
}
/* ---- states ---- */
:host([data-state="thinking"]) { --_dur: 0.95s; }
:host([data-state="streaming"]) { --_dur: 0.55s; }
:host([data-state="streaming"]) .glow {
opacity: 1;
filter: blur(0.5px);
}
:host([data-state="streaming"]) .outer,
:host([data-state="streaming"]) .mid,
:host([data-state="streaming"]) .core {
filter: saturate(1.15) brightness(1.06);
}
/* Mechanical “tool” feel without cardboard steps on organic fire */
:host([data-state="tool-calling"]) { --_dur: 0.72s; }
:host([data-state="tool-calling"]) .outer { animation-duration: calc(0.55s * var(--orbux-speed-scale)); }
:host([data-state="tool-calling"]) .mid { animation-duration: calc(0.42s * var(--orbux-speed-scale)); }
:host([data-state="tool-calling"]) .tip { animation-duration: calc(0.35s * var(--orbux-speed-scale)); }
:host([data-state="idle"]) .outer,
:host([data-state="idle"]) .mid,
:host([data-state="idle"]) .core,
:host([data-state="idle"]) .tip,
:host([data-state="idle"]) .glow,
:host([data-state="idle"]) .spark { animation: none; }
:host([data-state="idle"]) .flame { opacity: 0.48; }
:host([data-state="idle"]) .outer { transform: translateX(-50%) scale(0.88, 0.82); opacity: 0.55; }
:host([data-state="idle"]) .mid { transform: translateX(-50%) scale(0.9, 0.85); }
:host([data-state="idle"]) .core { transform: translateX(-50%) scale(0.92, 0.88); opacity: 0.7; }
:host([data-state="idle"]) .tip { opacity: 0.25; transform: translateX(-50%) scale(0.8); }
:host([data-state="idle"]) .glow { opacity: 0.35; transform: scaleX(0.85); }
:host([data-state="waiting"]) .outer,
:host([data-state="waiting"]) .mid,
:host([data-state="waiting"]) .core,
:host([data-state="waiting"]) .tip,
:host([data-state="waiting"]) .glow,
:host([data-state="waiting"]) .spark { animation: none; }
:host([data-state="waiting"]) .flame { opacity: 0.82; }
:host([data-state="waiting"]) .outer { transform: translateX(-50%) scale(0.92, 0.78); opacity: 0.7; }
:host([data-state="waiting"]) .mid { transform: translateX(-50%) scale(0.94, 0.8); }
:host([data-state="waiting"]) .core { transform: translateX(-50%) scale(0.9, 0.82); opacity: 0.85; }
:host([data-state="waiting"]) .tip { opacity: 0.4; transform: translateX(-50%) scale(0.75) rotate(-6deg); }
:host([data-state="waiting"]) .glow { opacity: 0.55; }
:host([data-state="done"]) {
--_tone: var(--orbux-color-success, #22c55e);
--_tone-2: #bbf7d0;
--_hot: #ecfdf5;
}
:host([data-state="error"]) {
--_tone: var(--orbux-color-error, #ef4444);
--_tone-2: #fbbf24;
--_hot: #fff7ed;
}
:host([data-state="done"]) .outer,
:host([data-state="error"]) .outer,
:host([data-state="done"]) .mid,
:host([data-state="error"]) .mid,
:host([data-state="done"]) .core,
:host([data-state="error"]) .core,
:host([data-state="done"]) .tip,
:host([data-state="error"]) .tip,
:host([data-state="done"]) .glow,
:host([data-state="error"]) .glow,
:host([data-state="done"]) .spark,
:host([data-state="error"]) .spark { animation: none; }
:host([data-state="done"]) .flame { animation: orbux-ember-done 220ms var(--_ease-out) 1; }
:host([data-state="error"]) .flame { animation: orbux-ember-error 200ms var(--_ease-out) 1; }
:host([data-settled="true"]) .flame { animation: none; }
:host([data-paused="true"]) .outer,
:host([data-paused="true"]) .mid,
:host([data-paused="true"]) .core,
:host([data-paused="true"]) .tip,
:host([data-paused="true"]) .glow,
:host([data-paused="true"]) .spark { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .outer,
:host([data-reduced-motion="true"]) .mid,
:host([data-reduced-motion="true"]) .core,
:host([data-reduced-motion="true"]) .tip,
:host([data-reduced-motion="true"]) .glow,
:host([data-reduced-motion="true"]) .spark { animation: none !important; }
:host([data-reduced-motion="true"]) .outer { transform: translateX(-50%); opacity: 0.85; }
:host([data-reduced-motion="true"]) .mid { transform: translateX(-50%); }
:host([data-reduced-motion="true"]) .core { transform: translateX(-50%); }
:host([data-reduced-motion="true"]) .tip { transform: translateX(-50%); opacity: 0.7; }
:host([data-reduced-motion="true"][data-state="streaming"]) .glow { opacity: 1; }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .outer { transform: translateX(-50%) scale(1.05, 0.92); }
:host([data-reduced-motion="true"][data-state="waiting"]) .outer,
:host([data-reduced-motion="true"][data-state="waiting"]) .mid,
:host([data-reduced-motion="true"][data-state="waiting"]) .core {
transform: translateX(-50%) scale(0.92, 0.8);
opacity: 0.7;
}
:host([data-reduced-motion="true"][data-state="idle"]) .flame { opacity: 0.5; }
`;
export class OrbuxEmberFlame extends OrbuxElement {
protected build(): void {
const style = document.createElement('style');
style.textContent = STYLES;
const flame = document.createElement('div');
flame.className = 'flame';
flame.setAttribute('part', 'flame');
const sparks = [
{ i: 0, x: '0%', s: '0%', drift: '28%' },
{ i: 1, x: '18%', s: '1%', drift: '-22%' },
{ i: 2, x: '8%', s: '0%', drift: '48%' },
{ i: 3, x: '28%', s: '1.5%', drift: '-36%' },
{ i: 4, x: '12%', s: '0%', drift: '18%' },
];
flame.innerHTML =
'<span class="glow" part="glow"></span>' +
'<span class="tongue outer" part="outer"></span>' +
'<span class="tongue mid" part="mid"></span>' +
'<span class="tongue core" part="core"></span>' +
'<span class="tongue tip" part="tip"></span>' +
sparks
.map(
(s) =>
`<i class="spark" style="--_i:${s.i};--_x:${s.x};--_s:${s.s};--_drift:${s.drift}" part="spark"></i>`,
)
.join('');
this.root.append(style, flame);
}
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-ember-flame')) {
customElements.define('orbux-ember-flame', OrbuxEmberFlame);
}
declare global {
interface HTMLElementTagNameMap {
'orbux-ember-flame': OrbuxEmberFlame;
}
}
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.