GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Canvas Fire Animation Scenes, No WebGL

A canvas fire animation here is a rule, not a sprite — no libraries, no WebGL — canvas particle math only: fuel burns down, buoyancy pushes particles up, and a

Auto-plays · click a tile to jump to its section · all nine in one zip

Why one fuel-cell rule branches into nine flames

The nine scenes climb one axis: how many things are pushing the fire around. 01's candle only has a fuel particle cooling as it rises — one force, buoyancy. 02 adds a curl-noise wind field so three flame columns lean into each other's turbulence. 03 and 07 both carry a wind vector, but 03's is a fixed direction with a slow wobble while 07's is measured live from pointermove and falls off with distance. 04 stacks radial-gradient light layers instead of moving particles at all. 05 and 09 leave particles behind entirely and simulate fire as a grid of cells trading heat and fuel — closer in spirit to a heat-diffusion terrain field than to a particle system, and a useful contrast with a velocity-grid fluid solver that advects density instead of burning it. 06 and 08 spend their particle budget on a single burst and a single moving head. Read them in order and the ninth scene isn't a new trick — it's the first candle's cooling rule running on a few thousand grid cells instead of a few hundred particles.

Why draw fire by computation? — fuel cells, buoyancy, curl turbulence

A fuel cell is one number counting down

Every scene above, particle or grid, tracks the same idea under a different name: a value that starts near 1 and burns toward 0 on a timer, with color and size read straight off that number. Once fuel is a number instead of a sprite frame, a fire animation css search doesn't need a spritesheet at all — the color ladder, the shrinking radius, and the eventual extinguishing all fall out of one subtraction per frame.

Buoyancy is an acceleration only where the field pulls on it

Setting a particle's vy to one constant at birth and never touching it again makes every particle rise at the same dead-even speed — a candle that looks like a row of tiny rockets. 01, 03, and 04 avoid that by rolling a different random vy for each particle at spawn, so speed varies between particles even though any one particle's own speed never changes afterward. 02 goes a step further and makes buoyancy an actual acceleration: every frame it pulls vy toward the curl field's target with Math.min(1, dt * 3.2) interpolation and adds a constant BUOY * 190 push on top, which is what lets its curl-driven wobble and upward force blend into one motion instead of reading as two separate effects.

Curl turbulence rotates without ever converging

02's wind field is built from the curl of a scalar potential, v = (∂ψ/∂y, −∂ψ/∂x), and that construction has one useful property: it has zero divergence everywhere. A flame effect css animation built on a plain random-walk wind will occasionally bunch every particle into one spot; a divergence-free field can only rotate mass around, so three flame columns swirl into each other without ever collapsing into a single blob.

Grid diffusion replaces particles with cells entirely

05 and 09 never allocate a Particle object — they store fuel and heat as two flat arrays the size of the grid and let each cell average its neighbors every step. A particle fire effect scales with how many particles you draw; a grid scales with how many cells you allocate, which is why 09's 120-column fuel grid can look busier than any of the particle scenes while touching a fixed, small amount of memory per frame.

Item code

Each item below names the exact constant that controls its behavior and shows the function that does the real work, copied from the running demo's source rather than retyped — so this page works like a fire animation codepen you lift from directly, not a screenshot of one.

01One candle — the smallest fire standing on a wick

Particles born at the wick rise by buoyancy and cool from yellow through orange to red as they age — that single rule is already a candle, and a background candle for meditation and night-mood pages needs nothing more. Every particle spawns with fuel k = 1 and loses K_BURN = 2.3 of it per second, with the color ladder reading that one number to go gold, amber, ember.

One candle — the smallest fire standing on a wick
Open in a new tab

01 · One candle — the smallest fire standing on a wick — Measured area 38.292% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillRectglobalCompositeOperationrequestAnimationFrame

Fuel is the only variable a candle needs

var K_BURN = 2.3;      // fuel burn rate (1/s)
var STILL_T = 1.6;     // frame drawn under reduced motion — where the halo reads best

for (var j = 0; j < N_PART; j++) {
  var q = parts[j];
  q.k -= K_BURN * dt;
  if (q.k <= 0) { spawn(q); continue; }
  var sway = Math.sin(tSec * 7.3 + q.ph) * 8 * q.k;
  var fx = cx + q.x + q.vx * q.k + sway + wind * (1 - q.k) * 26;
  var fy = wickTop + q.y * (0.6 + 0.4 * power);
  q.x += q.vx * dt;
  q.y += q.vy * LIFT * dt;
}

02Bonfire — a flame column shaken by curl noise

Sampling the curl of a potential field to blow each particle along the tangent makes logs of flame roll together into one heaving bonfire — a hero backdrop for camping and outdoor landings. The field is six summed sine waves whose curl is divergence-free, so the N_COL = 3 flame columns swirl into each other without ever collapsing into a point.

Bonfire — a flame column shaken by curl noise
Open in a new tab

02 · Bonfire — a flame column shaken by curl noise — Measured area 37.507% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillRectglobalCompositeOperationrequestAnimationFrame

Curl noise keeps the wind rotating, never converging

function curl(x, y, t) {
  // coordinates normalized to field units — the swirl scale stays fixed
  // no matter how big the canvas gets
  var u = 0, v = 0;
  var fx = x / 180, fy = y / 180;
  for (var i = 0; i < NW; i++) {
    var c = Math.cos(nkx[i] * fx + nky[i] * fy + nw[i] * t + nph[i]);
    u += namp[i] * nky[i] * c;
    v -= namp[i] * nkx[i] * c;
  }
  return [u, v];   // tangent wind (px/s)
}

03Torch — a flame carrying a wind vector on its back

Adding a slow wobble to a steady wind vector sweeps the whole flame to one side — the leaning blaze of a carried torch, at home on ceremonial game and event pages. WIND_VX = 46 never changes on its own; only a slow rotation of its angle breathes a few degrees each way.

Torch — a flame carrying a wind vector on its back
Open in a new tab

03 · Torch — a flame carrying a wind vector on its back — Measured area 34.055% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillRectglobalCompositeOperationrequestAnimationFrame

A constant vector plus one rotation term

var baseAng = Math.atan2(WIND_VY, WIND_VX) + dirTilt;
var wobAng = baseAng + Math.sin(tSec * 0.7) * 0.35;
var windMag = Math.hypot(WIND_VX, WIND_VY) * strength;
var windVX = Math.cos(wobAng) * windMag;
var windVY = Math.sin(wobAng) * windMag;

var fx = cx + q.x + q.vx * q.k + sway
       + windVX * lean * 0.55 * dt * 60;
var fy = poleTop + q.y * q.k
       + windVY * lean * 0.4 * dt * 60;
q.x += (q.vx + windVX * lean * 0.4) * dt;
q.y += (q.vy + windVY * lean * 0.3) * dt;

04Lamp flame — a halo bleeding behind the glass

Stacking radius-falloff light around one small flame turns it into a lamp scene where the wavering fire warms everything near the glass — quiet shop and hotel intro screens want a light source, not a spark shower. LAYERS = 11 gradient rings share one center that tracks the flame's sway, each alpha falling by Math.pow(R0 / R, FALLOFF) so the outermost ring never fully dies.

Lamp flame — a halo bleeding behind the glass
Open in a new tab

04 · Lamp flame — a halo bleeding behind the glass — Measured area 43.983% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillRectcreateRadialGradientglobalCompositeOperation

Radius falloff instead of moving particles

var R0 = Math.min(W, H) * 0.11 * breathe;
var A0 = 0.52 * (0.8 + 0.2 * flick) * (0.86 + 0.14 * Math.sin(tSec * 2.6 + 1.1));
var RMAX = Math.hypot(W, H) * 0.98;   // last ring reaches the far corner

for (var L = 0; L < LAYERS; L++) {
  var Lt = L / Math.max(1, LAYERS - 1);
  var R = R0 + (RMAX - R0) * Math.pow(Lt, 0.72);
  var a = Math.max(0.06, A0 * Math.pow(R0 / R, FALLOFF));
  // ring L drawn here with a radial gradient at radius R, alpha a
}

05Lava flow — heat piling up in grid cells

Letting neighboring cells exchange heat and coloring only the cells past a brightness threshold grows a lava flow that solidifies and remelts, all on a low-res grid — game backdrops and volcano-concept intros get a slow, geological flow instead of a particle spray. Cells average their four neighbors weighted so heat drifts upward, and T_MELT = 0.62 decides which cells glow orange and which harden back to rock.

Lava flow — heat piling up in grid cells
Open in a new tab

05 · Lava flow — heat piling up in grid cells — Measured area 39.884% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

Neighbor averaging plus proportional cooling

for (var yy = 1; yy < GH - 1; yy++) {
  for (var xx = 1; xx < GW - 1; xx++) {
    var i2 = yy * GW + xx;
    var c = heat[i2];
    var nU = heat[i2 - GW], nD = heat[i2 + GW], nL = heat[i2 - 1], nR = heat[i2 + 1];
    var avg = (nU * 0.9 + nD * 1.1 + nL + nR) / 4;
    var nh = c + K_DIFF * (avg - c);
    nh = nh - nh * COOL * (0.6 + nh);   // hotter cells radiate more
    heat2[i2] = nh < 0 ? 0 : (nh > 1.4 ? 1.4 : nh);
  }
}

06Firework burst — the arithmetic of gravity and embers

Fragments ejected from one point, falling under gravity and cooling through the color-temperature ladder, are the entire recipe for a night-sky burst — the highlight of celebration and year-end landings comes from that fall, not from a sprite sheet. PER_BURST = 170 fragments leave on a ring of angles and GRAV = 34 pulls every one of them down while their fuel walks from white through gold to ember.

Firework burst — the arithmetic of gravity and embers
Open in a new tab

06 · Firework burst — the arithmetic of gravity and embers — Measured area 39.459% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillRectglobalCompositeOperationrequestAnimationFrame

One burst, radial angles and two speed shells

function burst(x, y, tone) {
  var n = PER_BURST;
  var placed = 0;
  for (var i = 0; i < MAXP && placed < n; i++) {
    var p = pool[i];
    if (p.live) continue;
    var ang = (placed / n) * TAU + (rand() - 0.5) * 0.22;
    var shell = placed % 5 === 0 ? 0.62 : 1.0;   // inner ring falls slower
    var sp = (46 + rand() * 58) * shell;
    p.live = true;
    p.x = x; p.y = y;
    p.vx = Math.cos(ang) * sp;
    p.vy = Math.sin(ang) * sp - 12;
    p.k = 0.85 + rand() * 0.3;
    p.tone = tone;
    placed++;
  }
}

07Candle and wind — a flame that leans toward the pointer

Feeding the pointer-to-flame direction in as the wind vector, scaled by distance, gives a candle that lies down whenever a hand approaches — interactive cards and special-moment decor use it to react to an actual cursor. It is the one item reading a real pointermove position, with force falling off as Math.pow(60 / dist, FALLOFF) so a hand right over the wick pushes hard while one at the edge barely stirs it.

Candle and wind — a flame that leans toward the pointer
Open in a new tab

07 · Candle and wind — a flame that leans toward the pointer — Measured area 51.817% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointermovefillRectglobalCompositeOperation

Inverse-square wind from a measured pointer

function pointerWind() {
  if (px < 0 || py < 0) return [0, 0];
  var wickX = W * 0.5, wickY = H * 0.62;
  var dx = wickX - px, dy = wickY - py;
  var dist = Math.sqrt(dx * dx + dy * dy);
  if (dist < 1) return [0, -WIND_MAX];
  var s = Math.min(WIND_MAX, WIND_MAX * Math.pow(60 / dist, FALLOFF));
  return [(dx / dist) * s, (dy / dist) * s];
}

08Meteor — a fire trail that leaves an afterimage

Trailing particles behind a head and letting the path cool slowly draws a meteor burning through the atmosphere right across the screen — a backdrop for night-sky and weather-concept pages. It is the only scene measuring 21 of 23 animated frames instead of 23, because the head's Lissajous path pauses near the frame edges while TAIL_N = 42 trail particles cool at COOL = 0.017 per frame.

Meteor — a fire trail that leaves an afterimage
Open in a new tab

08 · Meteor — a fire trail that leaves an afterimage — Measured area 47.778% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillRectglobalCompositeOperationrequestAnimationFrame

A Lissajous head, not a straight line

function headAt(tSec) {
  var u = tSec * 0.5;
  return [
    W * (0.5 + 0.72 * Math.sin(u * 0.9)),
    H * (0.5 + 0.54 * Math.sin(u * 1.7 + 0.6))
  ];
}

09Ember spread — a grid of fuel cells burning outward

Storing fuel per cell and repeating the rule that burning cells pass heat to neighbors while their fuel drains approximates a fire eating across a map — game state screens and simulation intros can watch a fire actually consume a map instead of looping a texture. A cell spreads only once its heat crosses IGNITE = 0.34, handing heat to eight neighbors with diagonals divided by 1.41 because they sit farther away on the grid.

Ember spread — a grid of fuel cells burning outward
Open in a new tab

09 · Ember spread — a grid of fuel cells burning outward — Measured area 52.501% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

Eight-neighbor heat transfer, diagonals discounted

if (heat[i] > IGNITE) {
  if (f > 0) {
    var burn = Math.min(f, BURN_RATE * (0.4 + f));
    fuel[i] = f - burn;
    var give = SPREAD * (0.3 + f * 0.7);
    for (dy = -1; dy <= 1; dy++) {
      for (dx = -1; dx <= 1; dx++) {
        if (!dx && !dy) continue;
        nx = (i % COLS) + dx; ny = ((i / COLS) | 0) + dy;
        if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
        ni = ny * COLS + nx;
        nheat[ni] += give / (dx && dy ? 1.41 : 1);   // diagonal is farther
      }
    }
  }
}

Where a fire effect actually earns its place

A success message or an event page's celebration moment fits 06's firework burst — gravity and embers scattering apart — fired once on a completed checkout or a reached goal, never on a loop. A dark-mode toggle or a light-icon slot suits 04's lamp flame, a small scene where only the halo bleeds through the glass.

For a quiet accent in a hero background, 08's meteor leaves a trail with an afterimage, or 01's single candle starts at the smallest unit the set has — either one works laid in at low intensity.

Where the flame breaks once buoyancy is pushed too far

A fuel burn rate that's too high snaps the trail

06's fireworks originally burned fuel at 0.62 per second; dropped to BURN = 0.34 so a fragment survives long enough to still be on screen next frame. Push the burn rate back up and the trail behind a falling ember starts skipping — each particle vanishes and respawns before the eye can connect one frame to the next, so what should read as one falling spark reads as several unrelated flickers.

A diffusion coefficient set too low leaves half the grid dark

05's lava grid needs K_DIFF high enough that heat actually reaches the far edges of the 96-column grid before it cools away; the constant that ships is 0.34, cb4svrhm tuned up from 0.24 specifically because the lower value left the outer third of the grid never crossing T_MELT at all — a lava flow that never spreads past its vents isn't a flow, it's a static gradient with some noise on it.

A narrow grid makes the diagonal-neighbor discount visible

09's eight-neighbor spread divides diagonal heat transfer by 1.41 (√2, the extra distance a diagonal neighbor sits at) so the fire doesn't spread faster on the diagonals than straight across. Narrow that grid's column count much below 120 and the discount becomes visible as a faint plus-shaped bias in the burn front, because there just aren't enough cells between a diagonal step and a straight one to blur the difference away.

Three particle-count variants and their reduced-motion fallback

One constant, three different fires

Variant Changed value What it feels like
Particle count 01 candle particles 240 → 480 The flame thickens, at the cost of a heavier frame
Buoyancy 02 bonfire lift coefficient 0.35 → 0.7 The flame column doubles in height but thins out
Color $fire-amber → $fire-rose The orange flame shifts into a rose-toned candle

Change one constant and the same math hands you a different fire — the honest advantage over a flame effect css keyframe set that has to be re-authored by hand for every mood.

What reduced-motion keeps

Every one of the nine scenes checks window.matchMedia('(prefers-reduced-motion: reduce)').matches once on load, and if it's true the animation loop never starts — instead each item calls its draw function exactly once at a chosen still moment (STILL_T for the particle scenes, STILL_TICK steps of the simulation for 05's grid) so a reduced-motion visitor still sees a fire, just not one that moves. No item falls back to a blank canvas.

Keyboard and pointer both drive the interactive scenes

07's wind vector and 05's manual heat vent both read pointer coordinates through the same field element, which also carries tabindex="0" and role="application" so a keyboard user landing on the demo isn't stuck behind a mouse-only interaction; 01–04 and 06 read the same pointer coordinates too, folded into wind, power, or a click-triggered burst, and only 08 and 09 skip pointer input entirely — 08 answers to arrow keys and 09 to Enter or Space, both keyboard-only.

Questions people ask before wiring up a fire effect

What's the difference between a css fire animation and a canvas fire animation?

A CSS fire animation keyframes a fixed set of DOM elements or a background gradient, so the shapes it can produce are limited to what you hand-authored. A canvas fire animation recomputes every pixel from a rule each frame — a fuel value burning down, heat spreading between grid cells — so the flame's shape is an output of the simulation, not a set of pre-drawn frames.

Can I paste this fire animation codepen-style code in and use it as-is?

Yes — every code block above is copied from the running demo's actual source, not retyped, and each item's full file (HTML, SCSS, and JS) is inside the zip behind the password on this page. None of the nine scenes load an external library, so pasting one item's script tag and canvas markup into a page is the entire integration step.

Why does item 08's meteor measure 21 of 23 animated frames instead of 23?

Its head moves on a Lissajous curve, and the curve's own turning points near the frame edges hold the head almost still for a couple of the 23 sampled frames — the trail is still cooling and shrinking there, just not translating far enough between two adjacent samples to register as a "moved" frame the way a straight, constant-speed path would.

Sources: MDN — ImageData · MDN — CanvasRenderingContext2D.putImageData() · MDN — Window.requestAnimationFrame() · MDN — prefers-reduced-motion · Fabien Sanglard — the Doom fire PSX effect · Bridson & Simard — Curl-Noise for Procedural Fluid Flow, SIGGRAPH 2007 · a scene from the same ui/creative shelf as this one's neighboring canvas physics posts.

Enter the archive password

The password is inside this article. You will find it as you read.