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

CSS Smoke Effect: 9 Canvas Noise Patterns

A css smoke effect set: This smoke has no physics engine. Item 06 still swirls away from the pointer, item 07 still puffs out on a button press, and item 09

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

9 CSS Smoke Effects — How One Canvas Calculation Reshapes How Smoke Moves

All nine smoke effects below share one canvas loop: spawn a particle, age it by a lifetime fraction, and paint it with a radial gradient — nothing here is a CSS @keyframes tween sitting on an element. Item 01 is the plainest read, a plume that just rises and blurs. By item 06 that same loop bends around a pointer-driven vector field, and item 09 spends the identical math on a full scroll-bound scene, sharing its per-frame gradient-paint approach with Rainbow Gradient Background CSS: 9 Patterns. The order below moves from a static plume through wind, density, distance, color, pointer interaction, a burst trigger, depth layering, and a scroll-driven scene, so each pattern hands the loop one more input to read.

01Basic Smoke Plume That Rises Upward

As smoke particles rise from the bottom, they gradually spread sideways and blur more — PLUME_DRIFT steers that sideways wobble and PLUME_LIFE_MS caps how long a particle survives before SMOKE_RISE_SPEED_PX carries it past the top edge. Used as an ambient background accent, it only needs a dark backdrop for the glow to read.

Basic Smoke Plume That Rises Upward
Open in a new tab

01 · Basic Smoke Plume That Rises Upward — Measured area 12.548% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SMOKE_RISE_SPEED_PXPLUME_DRIFTPLUME_LIFE_MS
  function drawSmoke(nowMs) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);
    ctx.globalCompositeOperation = 'lighter';
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      var f = Math.max(0, Math.min(1, (nowMs - p.born) / PLUME_LIFE_MS));
      var turb = (valueNoise2D(p.wSeed * 0.03, nowMs * 0.0018) - 0.5) * 2;
      var x = p.bx + turb * PLUME_DRIFT * f * p.sign;
      var r = 7 + f * 32;
      var alpha = Math.sin(f * Math.PI) * 0.55;
      if (alpha <= 0.01) continue;
      var dx = x * sx, dy = p.y * sy, dr = r * ((sx + sy) / 2);
      var g = ctx.createRadialGradient(dx, dy, 0, dx, dy, dr);
      g.addColorStop(0, 'rgba(255,247,230,' + alpha + ')');
      g.addColorStop(1, 'rgba(255,247,230,0)');
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(dx, dy, dr, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalCompositeOperation = 'source-over';
  }

Item 01 repainted 12.5% of the frame at an average intensity of 25.8 across 23/23 measured frames.

02Smoke That Curls Sideways Along a Wind Flow Field

The flow-field strength is multiplied by a gust gain that rises and falls over time, so the stronger it is, the further the smoke column bends sideways as it climbs — WIND_FLOW_STRENGTH sets that ceiling and gustEnvelope(nowMs) is a time-based gain, not a slider a visitor drags. flowAngle() reads each particle's own position, so a column near the source bends less than one already high in the frame.

Smoke That Curls Sideways Along a Wind Flow Field
Open in a new tab

02 · Smoke That Curls Sideways Along a Wind Flow Field — Measured area 11.102% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WIND_FLOW_STRENGTHflowAnglegustEnvelope
  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    spawnAcc += dtMs * PLUME_SPAWN_PER_S / 1000;
    while (spawnAcc >= 1) { spawn(nowMs); spawnAcc -= 1; }
    var gust = gustEnvelope(nowMs);
    var alive = [];
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      var f = Math.max(0, Math.min(1, (nowMs - p.born) / PLUME_LIFE_MS));
      var ang = flowAngle(p.x, p.y, nowMs);
      p.x += Math.sin(ang) * WIND_FLOW_STRENGTH * gust * f * dtS;
      p.y -= SMOKE_RISE_SPEED_PX * dtS;
      if (nowMs - p.born < PLUME_LIFE_MS) alive.push(p);
    }
    particles = alive;
  }

Item 02 repainted 11.1% of the frame at an average intensity of 16.7 across 23/23 measured frames.

03Smoke Whose Thickness Changes With the Emission-Rate Value

Raising the emission-rate value packs more overlapping particles on screen, making the smoke read thicker and more opaque — SMOKE_EMISSION_RATE sets how many particles smkRate spawns per second, and DEMO_SWEEP_MS sweeps that value up and down on its own so the difference stays visible with no input. Used for a warning or alert-state background, thicker smoke reads as more urgent.

Smoke Whose Thickness Changes With the Emission-Rate Value
Open in a new tab

03 · Smoke Whose Thickness Changes With the Emission-Rate Value — Measured area 17.719% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SMOKE_EMISSION_RATEsmkRateDEMO_SWEEP_MS
  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    spawnAcc += dtMs * emissionRate / 1000;
    while (spawnAcc >= 1) { spawn(nowMs); spawnAcc -= 1; }
    var alive = [];
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      p.y -= SMOKE_RISE_SPEED_PX * dtS;
      if (nowMs - p.born < PLUME_LIFE_MS) alive.push(p);
    }
    particles = alive;
    if (particles.length > 320) particles = particles.slice(particles.length - 320);
  }

Item 03 repainted 17.7% of the frame at an average intensity of 16.2 across 23/23 measured frames.

04Smoke That Fades and Dissolves the Farther It Drifts From Its Source

Opacity drops in step with distance from the source, and past a set distance the particle disappears completely — DISSOLVE_DISTANCE is that cutoff, computed once from SMOKE_DISSOLVE_MS and DRIFT_SPEED_PX_S turning a duration into a travel distance. As a cleanup trail after loading completes, it clears itself without a manual fade-out.

Smoke That Fades and Dissolves the Farther It Drifts From Its Source
Open in a new tab

04 · Smoke That Fades and Dissolves the Farther It Drifts From Its Source — Measured area 15.542% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SMOKE_DISSOLVE_MSDISSOLVE_DISTANCEDRIFT_SPEED_PX_S
  function spawn() {
    var ang = -Math.PI / 2 + (pRand() - 0.5) * Math.PI * 0.9;
    particles.push({
      x: SOURCE_X, y: SOURCE_Y,
      vx: Math.cos(ang) * DRIFT_SPEED_PX_S,
      vy: Math.sin(ang) * DRIFT_SPEED_PX_S,
      wSeed: pRand() * 1000
    });
  }

  function stepSim(dtMs) {
    var dtS = dtMs / 1000;
    spawnAcc += dtMs * SPAWN_PER_S / 1000;
    while (spawnAcc >= 1) { spawn(); spawnAcc -= 1; }
    var alive = [];
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      p.x += p.vx * dtS;
      p.y += p.vy * dtS;
      var dist = Math.hypot(p.x - SOURCE_X, p.y - SOURCE_Y);
      if (dist < DISSOLVE_DISTANCE) alive.push(p);
    }
    particles = alive;
  }

Item 04 repainted 15.5% of the frame at an average intensity of 18.5 across 23/23 measured frames.

05Smoke Whose Color Shifts Over Time

The longer the smoke has been rising, the more its particle color drifts gradually between two set hues — SMOKE_TINT_CYCLE_MS sets how long a full cycle between SMOKE_TINT_FROM and SMOKE_TINT_TO takes. As a brand-color hero background, the shift stays slow enough to read as ambient rather than as an alert.

Smoke Whose Color Shifts Over Time
Open in a new tab

05 · Smoke Whose Color Shifts Over Time — Measured area 16.073% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SMOKE_TINT_CYCLE_MSSMOKE_TINT_FROMSMOKE_TINT_TO
    var sx = cw / CALC_W, sy = ch / CALC_H;
    var colorT = 0.5 + 0.5 * Math.sin(nowMs / SMOKE_TINT_CYCLE_MS * Math.PI * 2);
    var cr = Math.round(lerp(SMOKE_TINT_FROM[0], SMOKE_TINT_TO[0], colorT));
    var cg = Math.round(lerp(SMOKE_TINT_FROM[1], SMOKE_TINT_TO[1], colorT));
    var cb = Math.round(lerp(SMOKE_TINT_FROM[2], SMOKE_TINT_TO[2], colorT));
    ctx.clearRect(0, 0, cw, ch);
    ctx.globalCompositeOperation = 'lighter';
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      var f = Math.max(0, Math.min(1, (nowMs - p.born) / PLUME_LIFE_MS));
      var turb = (valueNoise2D(p.wSeed * 0.03, nowMs * 0.0018) - 0.5) * 2;
      var x = p.bx + turb * 30 * f * p.sign;
      var r = 7 + f * 32;
      var alpha = Math.sin(f * Math.PI) * 0.6;
      if (alpha <= 0.01) continue;
      var dx = x * sx, dy = p.y * sy, dr = r * ((sx + sy) / 2);
      var g = ctx.createRadialGradient(dx, dy, 0, dx, dy, dr);
      g.addColorStop(0, 'rgba(' + cr + ',' + cg + ',' + cb + ',' + alpha + ')');
      g.addColorStop(1, 'rgba(' + cr + ',' + cg + ',' + cb + ',0)');
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(dx, dy, dr, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalCompositeOperation = 'source-over';

Item 05 repainted 16.1% of the frame at an average intensity of 30.6 across 23/23 measured frames.

06Smoke That Swirls Away From the Pointer

The closer the pointer gets to the smoke column, the more nearby particles swirl and get pushed aside — SMOKE_SWIRL_RADIUS sets how far that push reaches and SMOKE_SWIRL_STRENGTH sets how hard it shoves, while virtualPointerOrbit drives the demo's own pointer when nobody is hovering. Arrow keys move that virtual pointer in all four directions, so the swirl works without a mouse too.

Smoke That Swirls Away From the Pointer
Open in a new tab

06 · Smoke That Swirls Away From the Pointer — Measured area 21.019% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SMOKE_SWIRL_RADIUSSMOKE_SWIRL_STRENGTHvirtualPointerOrbit
  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    spawnAcc += dtMs * PLUME_SPAWN_PER_S / 1000;
    while (spawnAcc >= 1) { spawn(nowMs); spawnAcc -= 1; }
    var alive = [];
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      var turb = (valueNoise2D(p.wSeed * 0.03, nowMs * 0.0018) - 0.5) * 2;
      p.x += turb * 8 * dtS;
      p.y -= SMOKE_RISE_SPEED_PX * dtS;
      var rx = p.x - pointerX, ry = p.y - pointerY;
      var dist = Math.hypot(rx, ry);
      if (dist < SMOKE_SWIRL_RADIUS && dist > 0.5) {
        var push = (1 - dist / SMOKE_SWIRL_RADIUS) * SMOKE_SWIRL_STRENGTH;
        var tx = -ry / dist, ty = rx / dist;
        p.x += tx * push * dtS;
        p.y += ty * push * dtS;
      }
      if (nowMs - p.born < PLUME_LIFE_MS) alive.push(p);
    }
    particles = alive;
  }

Item 06 repainted 21.0% of the frame at an average intensity of 29.6 across 23/23 measured frames.

07Smoke That Puffs Outward in a Burst When a Button Is Pressed

The instant the button is pressed, one particle cluster forms and bursts outward in every direction before quickly fading — SMOKE_BURST_COUNT sets the cluster size, SMOKE_BURST_LIFE_MS sets how fast it fades, and smkBtn answers a click, Enter, or Space the same way. As submit-complete button feedback, the burst reads once and gets out of the way.

Smoke That Puffs Outward in a Burst When a Button Is Pressed
Open in a new tab

07 · Smoke That Puffs Outward in a Burst When a Button Is Pressed — Measured area 25.765% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

SMOKE_BURST_COUNTSMOKE_BURST_LIFE_MSsmkBtn
  function burst(bornMs) {
    for (var i = 0; i < SMOKE_BURST_COUNT; i += 1) {
      var ang = pRand() * Math.PI * 2;
      var speed = SMOKE_BURST_SPEED_PX_S * (0.4 + pRand() * 0.9);
      particles.push({
        x: ORIGIN_X, y: ORIGIN_Y,
        vx: Math.cos(ang) * speed,
        vy: Math.sin(ang) * speed,
        born: bornMs
      });
    }
  }

  function stepSim(dtMs, nowMs) {
    var dtS = dtMs / 1000;
    var alive = [];
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      p.x += p.vx * dtS;
      p.y += p.vy * dtS;
      p.vy -= 8 * dtS;
      if (nowMs - p.born < SMOKE_BURST_LIFE_MS) alive.push(p);
    }
    particles = alive;
  }

Item 07 repainted 25.8% of the frame at an average intensity of 72.7 across 18/23 measured frames — lower than the others on purpose, since a burst decays to nothing well before the capture window ends and the trailing frames simply have no particles left to move.

08Layered Smoke Where Each Layer Moves at a Different Speed

Layers further back move their particles slower and blurrier, and that speed gap against the front layer reads as depth — LAYERS holds each layer's own particle pool, and radiusMul/alphaMul scale size and opacity per layer without touching the shared draw loop. As a portfolio hero scene, the depth reads even on a still frame.

Layered Smoke Where Each Layer Moves at a Different Speed
Open in a new tab

08 · Layered Smoke Where Each Layer Moves at a Different Speed — Measured area 23.225% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LAYERSradiusMulalphaMul
  function drawSmoke(nowMs) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);
    for (var li = 0; li < LAYERS.length; li += 1) {
      var L = LAYERS[li];
      for (var i = 0; i < L.particles.length; i += 1) {
        var p = L.particles[i];
        var f = Math.max(0, Math.min(1, (nowMs - p.born) / PLUME_LIFE_MS));
        var turb = (valueNoise2D(p.wSeed * 0.03, nowMs * 0.0016) - 0.5) * 2;
        var x = p.bx + turb * 14 * f;
        var r = (6 + f * 16) * L.radiusMul;
        var alpha = Math.sin(f * Math.PI) * L.alphaMul;
        if (alpha <= 0.01) continue;
        var dx = x * sx, dy = p.y * sy, dr = r * ((sx + sy) / 2);
        var g = ctx.createRadialGradient(dx, dy, 0, dx, dy, dr);
        g.addColorStop(0, 'rgba(30,28,34,' + alpha + ')');
        g.addColorStop(1, 'rgba(30,28,34,0)');
        ctx.fillStyle = g;
        ctx.beginPath();
        ctx.arc(dx, dy, dr, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }

Item 08 repainted 23.2% of the frame at an average intensity of 18.8 across 23/23 measured frames.

09Hero Smoke Scene That Reacts to the Scroll Value

As the scroll value grows, both the smoke emission rate and its spread radius increase together, thickening the whole scene's density — scrollProgress drives both numbers at once, and without a real scroll input triangleWave() rocks it back and forth on its own every SCROLL_AUTO_PERIOD_MS, while ArrowUp/ArrowDown and PageUp/PageDown move that same value by hand. As the site's top hero section, the scene keeps building density the deeper a visitor scrolls.

Hero Smoke Scene That Reacts to the Scroll Value
Open in a new tab

09 · Hero Smoke Scene That Reacts to the Scroll Value — Measured area 11.384% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

scrollProgresstriangleWaveSCROLL_AUTO_PERIOD_MS
    var emissionRate = BASE_EMISSION_PER_S + scrollProgress * EMISSION_RANGE_PER_S;
    var swirlRadius = SMOKE_SWIRL_RADIUS_BASE + scrollProgress * SMOKE_SWIRL_RADIUS_RANGE;
    spawnAcc += dtMs * emissionRate / 1000;
    var cap = 260;
    while (spawnAcc >= 1) {
      if (particles.length < cap) spawn(nowMs);
      spawnAcc -= 1;
    }
    var alive = [];
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      var turb = (valueNoise2D(p.wSeed * 0.03, nowMs * 0.0017) - 0.5) * 2;
      p.x += turb * 10 * dtS;
      p.y -= SMOKE_RISE_SPEED_PX * dtS;
      var rx = p.x - pointerX, ry = p.y - pointerY;
      var dist = Math.hypot(rx, ry);
      if (dist < swirlRadius && dist > 0.5) {
        var push = (1 - dist / swirlRadius) * SMOKE_SWIRL_STRENGTH;
        var tx = -ry / dist, ty = rx / dist;
        p.x += tx * push * dtS;
        p.y += ty * push * dtS;
      }
      if (nowMs - p.born < PLUME_LIFE_MS) alive.push(p);
    }
    particles = alive;

Item 09 repainted 11.4% of the frame at an average intensity of 18.0 across 23/23 measured frames.

Nine CSS Smoke Effects, Measured in Area and Intensity

Each row below pairs the share of the frame that repainted between captures with the average brightness change wherever that repaint landed. Item 07's burst is the most intense per pixel it touches but the shortest-lived, item 06's pointer swirl covers the most real estate of the ambient patterns, and item 09's hero scene stays modest on area because its density only grows as the scroll value does. A css smoke effect background reads calmest when area stays under 20% and intensity stays under 30, which is where most of these nine sit.

| Pattern | Cumulative area | Avg. intensity | Frames moved |

|---|---|---|---|

| 01 Ambient plume rise | 12.5% | 25.8 | 23/23 |

| 02 Wind flow-field drift | 11.1% | 16.7 | 23/23 |

| 03 Emission-rate density | 17.7% | 16.2 | 23/23 |

| 04 Distance fade dissolve | 15.5% | 18.5 | 23/23 |

| 05 Color tint shift | 16.1% | 30.6 | 23/23 |

| 06 Pointer swirl avoid | 21.0% | 29.6 | 23/23 |

| 07 Press puff burst | 25.8% | 72.7 | 18/23 |

| 08 Layered depth parallax | 23.2% | 18.8 | 23/23 |

| 09 Hero scroll scene | 11.4% | 18.0 | 23/23 |

Where the Values Change the Smoke — Emission Rate, Rise Speed, Dissolve Time

Three numbers below are doing more work than they look: SMOKE_EMISSION_RATE decides how many particles item 03 packs into one screen, SMOKE_RISE_SPEED_PX decides how fast item 01's column climbs and leaves the frame, and SMOKE_DISSOLVE_MS decides how far item 04 lets a particle drift before it's gone. None of them change what shape gets painted — they change how thick, how quick, and how far the same radial-gradient loop reads on screen, which is most of what separates a smoke effect css animation that feels considered from one that just loops the same puff forever. HTML5 Canvas Animation: 9 Warp & Droplet Effects tunes a similar kind of per-pixel calculation, just on a warped image instead of drifting particles.

Where It Breaks — Reduced Motion Settings, Frame Drops on Low-End Devices, Use Without a Background

prefers-reduced-motion strips the auto-playing .is-demo class from every item on load instead of disabling the canvas outright, so item 06 still swirls when a pointer actually moves over it and item 07 still puffs on a real click — only the scripted autoplay loop is skipped. Every item redraws every particle with its own createRadialGradient() call, and globalCompositeOperation: 'lighter' on top of that means a low-end device rendering hundreds of overlapping particles at once is the one place frame drops show up first, well before anything else on a page struggles. These particles read against darkness: drop any of the nine over a plain white page and the additive glow barely shows, since it's built to brighten a dark pixel rather than to sit as an opaque shape on a light one. The nine index.html/style.scss pairs behind these builds, 59z8ez3n, are the fastest way to see exactly how little a single canvas loop needs to carry all nine of these smoke effect css codepen-style demos.

Three Variants and Accessibility

| Variant | Value change | Feel |

|---|---|---|

| Adjust the smoke emission-rate value | 03 SMOKE_EMISSION_RATE 4 → 18 | Lowering the value keeps particle count low so the smoke reads thin and sparse, and raising it packs particles tightly so it reads thick and opaque. |

| Adjust the smoke rise-speed value | 01 SMOKE_RISE_SPEED_PX 12 → 40 | Lowering the value makes the smoke climb slowly and linger longer, and raising it makes it shoot upward and leave the screen quickly. |

| Adjust the smoke dissolve-time value | 04 SMOKE_DISSOLVE_MS 900 → 2600 | Lowering the value makes it disappear soon after leaving the source, and raising it lets it drift far before finally fading away. |

Every one of the nine checks prefers-reduced-motion once at the top of its own script and answers by dropping the autoplay class rather than turning off individual properties, so a visitor who asked for less motion still sees the shape settle at rest instead of a blank canvas. Arrow keys move item 06's virtual pointer in all four directions, Enter or Space triggers item 07's burst exactly like a click, and item 09 answers ArrowUp/ArrowDown as well as PageUp/PageDown to move its scroll value without a real scrollbar. CanvasRenderingContext2D, requestAnimationFrame(), and prefers-reduced-motion are documented on MDN.

Does a CSS Smoke Effect Need a Particle Library?

None of these nine load one. Each keeps its own array of plain objects, ages them with Math.max(0, Math.min(1, (nowMs - p.born) / PLUME_LIFE_MS)), and drops the ones whose time is up — the same pattern a particle library wraps in its own API, written out in the open instead. Canvas Fluid Simulation: 9 Grid Effects runs that same no-library approach across a full pixel grid instead of individual particles, which is the next step up once nine drifting points stop being enough.

Questions Worth Answering Before You Copy One of These Nine

Can I drop one of these nine into a page as an html css smoke effect without any build step?

Yes. Every item is a self-contained index.html with its own inline <canvas>, script, and stylesheet — open it in a browser and it runs, no bundler or framework required.

Why does item 07 show fewer moved frames than the rest?

Its burst finishes fading well inside the fixed 23-frame measurement window, so the last several captured frames genuinely have no particles left to move — that's the burst working as designed, not a missed measurement.

Do I need WebGL for a css smoke effect background like these?

No. All nine run on the plain 2D CanvasRenderingContext2D API with globalCompositeOperation: 'lighter' for the glow — there's no WebGL context or shader anywhere in the nine builds.

Enter the archive password

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