Atmosphere✨ Premium

Smoke

A 2D canvas grows semi-transparent circular smoke particles with sinusoidal lateral drift and radial expansion — pure rAF, zero dependencies.

JSCanvasSmoke

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

Hero / Landing — Dark-brand atmosphere — Smoke example 1

① Hero / Landing — Dark-brand atmosphere

WhenHero section of a dark-identity site (spirits, perfumery, fiction, gaming) where visual atmosphere takes precedence over dense information.
WhyTranslucent smoke rises without interaction, builds ambient depth, and does not compete with text — zero external images, zero network, minimal CPU load.
SettingsRGB 160,120,80 for amber smoke; decay: 0.002 + 0.002 * Math.random() for long-lived particles (200–500 frames); spawn centred under the source object (candle, incense, burner).
Product component — Async processing card — Smoke example 2

② Product component — Async processing card

WhenA long operation is running (PDF export, transcription, video render) and the status card needs to signal activity without a standard spinner.
WhySmoke rising from a 'forge' or 'processor' icon replaces the loader with a craft/industrial aesthetic that stands out in a technical dashboard.
Settingsvx: 0.2 * (Math.random() - .5) for a straighter plume (±0.1 px); Math.random() > 0.6 for reduced density (40 % spawn); initial size 3 * Math.random() + 2 (2–5 px) for fine, delicate wisps.
Conversion micro-interaction — Premium content gate — Smoke example 3

③ Conversion micro-interaction — Premium content gate

WhenContent locked behind a premium account: the visitor sees a blurred preview wrapped in dense smoke and a 'Reveal' CTA.
WhyThick smoke reinforces the mystery of the hidden content and builds visual tension that motivates the unlock action.
SettingsMath.random() > 0.1 (90 % spawn) for high density; vy: -0.8 * Math.random() - 0.3 for slow rise and stagnant smoke; opacity raised to 0.25 * a.life for a noticeably denser veil.

How it works

The IIFE initialises the canvas via setupCanvas("psSmoke") — the function reads the parent's getBoundingClientRect() to set width/height, then returns {canvas, ctx, w, h}. An array e accumulates live particles. The requestAnimationFrame loop repaints every frame: clearRect then fillRect in #0a0a0f erase the previous frame, then all particles are updated and drawn before expired ones are removed by reverse splice.

Each frame, if Math.random() > 0.3 (70 % probability), a particle is pushed near the bottom centre — x = w/2 ± 5 px, y = h − 30. Its properties: horizontal drift vx = ±0.25 px/frame, upward velocity vy between −0.5 and −2.0 px/frame, life = 1, decay between 0.003 and 0.008 (lifespan 125–333 frames), initial size 4–12 px.

Each frame, the position evolves as x += vx + 0.3 × sin(0.02 × y) — the sinusoidal component keyed to Y produces a progressive undulating drift that simulates natural smoke turbulence. y += vy (ascent), life -= decay, size += 0.15 (radial expansion). Each particle is a filled arc() in rgba(180,180,190, 0.15 × life): 15 % max opacity at birth, invisible at death.

The code handles neither prefers-reduced-motion, nor IntersectionObserver, nor canvas resizing after init. The IIFE targets a single canvas by fixed id (id="psSmoke") — for multiple simultaneous columns, the particle loop must be extracted into a function parameterised by id.

Accessibility

  • prefers-reduced-motion not handled: the code does not check this OS preference — the animation loops indefinitely regardless of accessibility settings. Fix: add if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; before launching the rAF.
  • The canvas carries neither aria-hidden="true" nor a role in the source HTML — add aria-hidden="true" for any decorative use to exclude the element from the accessibility tree.
  • No pause/stop control is exposed — WCAG 2.2.2 requires a pause mechanism for any auto-playing animation lasting more than 5 seconds.
  • No keyboard interaction by design (purely decorative effect) — suitable as an ambient background provided the canvas is hidden from the accessibility tree via aria-hidden.
  • Low particle opacity (rgba(180,180,190) at 15 % max) — the smoke does not obscure text content overlaid on a dark background, limiting visual noise for contrast-sensitive users.

Browser compatibility

Requires Canvas 2D Context and requestAnimationFrame — supported in all modern browsers. Zero external dependencies.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 15+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Touch OK
Android Chrome✓ Full

Without Canvas support, the container renders empty on a dark background — no fatal JS error, any overlaid text content remains readable.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="demo-preview">
  <div class="ps-canvas-wrap">
    <canvas class="ps-canvas" id="psSmoke" width="392" height="280"
            aria-hidden="true"></canvas>
  </div>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
rgba(180,180,190, ...) grey-blue RGB values in ctx.fillStyle — use 160,120,80 for amber smoke, 100,130,180 for blue smoke, 200,60,40 for reddish volcanic vapour.
0.15 * a.life 0.15 Max opacity multiplier — increase to 0.35 for dense visible smoke, drop to 0.06 for barely perceptible background wisps.
Math.random() > .3 0.3 (70 % spawn) Spawn threshold per frame — raise to 0.8 (20 % chance) for sparse distant smoke, lower to 0.05 (95 %) for a dense continuous column.
vy: 1.5 * -Math.random() - .5 −0.5 to −2.0 px/frame Rise speed — multiply the 1.5 coefficient by 0.4 for lazy smoke, by 2.5 for brisk vapour.
.3 * Math.sin(.02 * a.y) 0.3 px amplitude Sinusoidal drift amplitude — set to 0 for a straight vertical plume, to 1.2 for a highly serpentine, twisting smoke.
size: 8 * Math.random() + 4 4–12 px Initial particle size — use 20 * Math.random() + 10 for large billowing clouds, 3 * Math.random() + 1 for precise thin wisps.
size += .15 0.15 px/frame Radial expansion rate — increase to 0.4 for smoke that billows outward quickly, set to 0 for fixed-size floating mist particles.

FAQ

The source code targets a single canvas via getElementById("psSmoke") — the IIFE can only run once. For multiple columns, extract the particle loop into a function taking an id parameter and call it for each canvas. The internal logic is fully independent: each call maintains its own particle array and its own rAF.
Add before the requestAnimationFrame launch: if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;. For a degraded fallback (a single static frame), draw the initial particles without starting the loop. Since Chrome 76+, the MediaQueryList emits a change event — you can react in real time if the user updates the preference mid-session.
Replace fillStyle = `rgba(180,180,190,${i})` with fillStyle = `hsla(${200 + (a.life * 20) | 0},20%,75%,${i})` — the particle is born cool (HSL 220, bluish) and ages toward neutral grey (HSL 200) as it rises, simulating progressive cooling.