Fire Effect
2D canvas fire particles — lifetime-driven yellow→orange→red gradient, smoke trail via semi-transparent background fill, 3 spawns per frame from the base.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Atmosphere has 57 effects, including 10 free. Explore the category →
3 usage examples



How it works
On every frame, the canvas is not cleared with clearRect: a semi-transparent ctx.fillRect (rgba(10,10,15,0.15)) is applied instead. Previous frames persist at 85% opacity, producing the smoke trail effect with no extra buffer or storage.
3 new particles spawn each frame from the canvas bottom (y = height − 20 px), at a random x position ±20 px around center (t.w/2 + 40*(Math.random()−0.5)). Initial vertical velocity: −2 to −6 px/frame (rising); horizontal drift: ±1 px/frame. Deceleration is minimal: vy *= 0.99 — particles rise at near-constant speed until their lifetime runs out.
The color gradient is driven by remaining life: R = 255, G = floor(200 × life), B = floor(50 × life). Fresh (life = 1) → yellow-orange (255, 200, 50); at mid-life (0.5) → orange (255, 100, 25); near death → pure red fading out. Size and alpha decay in parallel: size *= 0.98 per frame, alpha = 0.8 × life. Each particle lives 33–100 frames (random decay 0.01–0.03).
The effect runs an unconditional requestAnimationFrame loop — no IntersectionObserver, no visibility check. There is no public API and no canvas resize handling after init: setupCanvas reads parent dimensions once at load.
Accessibility
- prefers-reduced-motion absent: the effect does not check this OS preference. The animation always runs, even when 'Reduce Motion' is active. Handle manually with
window.matchMedia('(prefers-reduced-motion:reduce)').matchesbefore starting the RAF. - The
<canvas>carries noaria-hidden,role, oraria-labelin the provided code — screen readers may announce it as an empty interactive element. Addaria-hidden="true"on the canvas androle="img"+aria-labelon the wrapper. - No keyboard or pointer interaction: the effect is purely decorative. It does not interfere with keyboard navigation elsewhere on the page.
- The brightest particles (yellow-orange on near-black #0a0a0f) exceed a 7:1 contrast ratio — high visual legibility.
- Information is encoded by color alone (particle age = color); no non-color alternative is provided. As a purely decorative effect, this is acceptable.
Browser compatibility
Requires Canvas 2D, requestAnimationFrame, and ES6 template literals — all modern browsers since 2016.
Without Canvas 2D, the <code><canvas></code> stays empty and silent — no fatal JS error. The <code>.ps-canvas-wrap</code> container still occupies its space; add a CSS fallback background (e.g. an orange-to-red gradient) if visibility is required.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview" role="img"
aria-label="Rising fire particle animation in yellow to red">
<div class="ps-canvas-wrap">
<canvas class="ps-canvas" id="psFire"
aria-hidden="true">
</canvas>
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| Spawn rate (JS: for loop) | 3 per frame | for (let n = 0; n < 3; n++) — raise to 5–8 for a dense wide flame, drop to 1–2 for a light flicker. |
| Horizontal spread (JS: 40) | ±20 px from center | 40 * (Math.random() − .5) — double to 80 for a wide flame, reduce to 10 for a needle-thin column centered on a button. |
| Rise speed (JS: vy) | -2 to -6 px/frame | 4 * -Math.random() - 2 — raise the multiplier (4→6) for faster particles; raise the offset (2→3) for a higher minimum speed. |
| Initial size (JS: size) | 3–9 px | 6 * Math.random() + 3 — first term sets variation range, second sets minimum. Large embers: 10 * Math.random() + 6. |
| Lifetime (JS: decay) | 0.01–0.03 per frame | .02 * Math.random() + .01 — lower for long-lived particles rising high (tall flame); raise for a short intense burst near the source. |
| Smoke trail (JS: background fillStyle) | rgba(10,10,15,0.15) | Semi-transparent overlay replacing clearRect. Lower alpha (0.06–0.10) for a long vaporous trail; raise it (0.30–0.50) to cut the smoke and show only fresh particles. |
| Color palette (JS: RGB formula) | yellow→orange→red | Replace rgba(${a},${l},${c},…) — blue-white fire: rgba(${Math.floor(200*i.life)},${Math.floor(200*i.life)},255,…); green fire: rgba(0,255,${Math.floor(200*i.life)},…). |
FAQ
psFire at load — only one instance runs natively. For multiple flames, extract the logic into a parameterized function: function initFire(id){ const t = setupCanvas(id); const e = []; (function n(){…requestAnimationFrame(n)})(); } then call initFire('fire1'), initFire('fire2'), etc. setupCanvas is a reusable global function.let rafId = requestAnimationFrame(n)) and cancel with cancelAnimationFrame(rafId). To restart, simply call the tick function again.setupCanvas reads the parent's getBoundingClientRect() once at init. If the container changes size (window resize, accordion panel), recalibrate manually: canvas.width = wrap.offsetWidth; canvas.height = wrap.offsetHeight; and update t.w and t.h. Add a ResizeObserver on .ps-canvas-wrap to automate this.