Canvas Snow Overlay — Free Drifting Snowflake Hero Background

Canvas Snow Overlay · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Click-through overlay
pointer-events: none keeps the UI beneath fully interactive.
Per-flake wind phase
Independent sine drift avoids a mechanical, synced sway.
High-DPI aware
devicePixelRatio scaling keeps flakes crisp on Retina.
Allocation-free recycling
Flakes reset in place instead of being spliced and re-added.
Varied size and opacity
Random radius and alpha give a sense of depth.
Reduced-motion fallback
A static frame draws when the OS asks for less motion.
Responsive canvas
Resizes and rescales its backing store on window resize.
Zero dependencies
Pure Canvas 2D and vanilla JS.

About this UI Snippet

Canvas Snow Overlay — Drifting Snowflakes Over Any Section

Screenshot of the Canvas Snow Overlay snippet rendered live

A snow overlay is one of the simplest particle effects to build and one of the easiest to get wrong — flakes that all fall at the same speed in the same straight line read as a bug, not weather. This snippet layers a transparent, non-blocking canvas over a hero section and gives each flake its own size, fall speed, drift rhythm, and opacity so the whole thing reads as gentle, ambient snowfall rather than a repeating sprite.

A transparent, click-through layer

The canvas is positioned absolute; inset: 0 inside a relatively-positioned wrapper, sized to match it exactly, and set to pointer-events: none. That last property is what makes it an overlay rather than an obstacle — every click and hover on the button and text underneath passes straight through the canvas, even though the canvas visually sits on top in z-index.

High-DPI without the blur

resize() reads window.devicePixelRatio (capped at 2 to avoid oversized backing stores on very high-density phones), sizes the canvas's backing store to clientWidth * DPR, and then calls ctx.setTransform(DPR, 0, 0, DPR, 0, 0) so every subsequent drawing call can keep using plain CSS-pixel coordinates. Skip that step and the snow renders soft and slightly blurred on Retina displays — a common canvas mistake that's invisible until you compare it side by side with a native image.

Why each flake needs its own phase

Every flake stores a drift speed and a driftPhase — a random starting angle for its personal sine wave. The horizontal sway comes from Math.sin(t * f.drift + f.driftPhase) * 0.6, so no two flakes swing left and right in unison. That single random phase offset is the entire trick behind the effect looking organic instead of mechanical, the same idea used in the layered waves of aurora background and the particle jitter of starfield.

Recycling instead of removing

Rather than deleting flakes that fall past the bottom edge and spawning new ones (which would need array splicing every frame), a flake that exits is simply reassigned fresh random properties and reset to y: -10 via makeFlake(false). The array length and each element's memory address never change, which keeps the loop allocation-free and steady at any flake count.

Respecting reduced motion

window.matchMedia('(prefers-reduced-motion: reduce)') is checked once on load. If the user has that OS preference set, the loop never starts — instead, one static frame of snow is drawn so the section still looks dressed for winter without any perpetual animation running in the background. Pair this overlay with a particle network hero for tech-brand pages, or dial down the flake count and speed for a subtle year-round texture layer.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why giving each flake its own random driftPhase for its sine-wave sway is what keeps the snowfall from looking synchronized, or why pointer-events: none on the overlay canvas matters for a hero section with a real call-to-action button underneath it. It's also a good partner for optimizing the loop — ask whether recycling flakes in place (resetting their properties instead of removing and re-pushing array entries) meaningfully avoids garbage collection pauses at this particle count, and whether devicePixelRatio should be capped lower on low-end mobile devices. For extensions, ask it to add a subtle "wind gust" that periodically increases the drift amplitude for a few seconds, vary flake shapes between simple dots and small hex-crystal outlines, or accumulate a snow-depth silhouette along the bottom edge that grows over time. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "canvas snow overlay" effect in plain HTML, CSS, and JavaScript using only the Canvas 2D API — a transparent, click-through layer of falling snowflakes over a hero section.

Requirements:
- An absolutely positioned canvas filling a relatively positioned hero wrapper, styled with pointer-events: none so every click and hover on the hero's real content (heading, paragraph, button) underneath passes through untouched.
- Size the canvas using window.devicePixelRatio (capped at 2) for the backing store, and call ctx.setTransform to compensate, so the snow renders crisp rather than blurry on high-DPI screens, and re-run this sizing on window resize.
- Maintain an array of roughly 100-150 flake objects, each with an independent random radius, fall speed, opacity, and a drift speed plus drift phase used to offset a per-flake sine wave for horizontal sway — explicitly avoid giving all flakes the same drift timing, since that is what makes generic snow effects look mechanical instead of organic.
- On each requestAnimationFrame tick, advance every flake's y position by its own speed and its x position by its own sine-based sway, wrapping horizontally if a flake drifts off either edge, and when a flake's y exceeds the canvas height, reset that same array element in place with fresh random properties at y approximately -10 rather than splicing it out and pushing a new object (to stay allocation-free).
- Check window.matchMedia('(prefers-reduced-motion: reduce)') once on load: if it matches, skip starting the animation loop entirely and instead draw one static frame of the seeded flakes, so users who have asked for less motion still see a snow-dressed hero without a perpetual animation.
- Style it as a dark winter hero section with a heading, supporting text, and a gradient call-to-action button, all rendered above the canvas in stacking order.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA hero section renders with snow drifting over its content.
  2. 2
    Watch the driftEach flake sways on its own sine phase, not in lockstep.
  3. 3
    Click the buttonpointer-events: none means clicks pass straight through the canvas.
  4. 4
    Resize the windowThe canvas rescales for devicePixelRatio and stays crisp.
  5. 5
    Enable reduced motion in your OSReload — the loop is skipped and one static frame draws instead.
  6. 6
    Tune the stormChange FLAKE_COUNT, speed range, and drift amplitude.

Real-world uses

Common Use Cases

Seasonal hero sections
Winter sale banners and holiday landing pages.
Header ambience
A subtle year-round texture layer over a dark header.
Alongside starfields
Swap for a starfield background off-season.
E-commerce campaigns
Layer over a hero promo without blocking the CTA.
Event landing pages
Set a wintry mood for a conference or product launch.
Email/social campaign previews
Record the canvas as a looping GIF or video export.

Got questions?

Frequently Asked Questions

The canvas is positioned absolutely over the entire hero section so it can draw snow above the text and button, but without pointer-events: none it would also intercept every click and hover meant for that content. Setting it to none makes the canvas purely visual — the browser routes all pointer events straight through to whatever is beneath it.

Each flake gets its own random size, fall speed, opacity, and — most importantly — its own drift phase, a random starting angle fed into a per-flake sine wave that controls horizontal sway. Because no two flakes share a phase, they never sway in unison, so the aggregate motion reads as organic wind rather than a looping sprite sheet.

Yes. On load it checks window.matchMedia for the prefers-reduced-motion: reduce media feature. If the user has that OS-level accessibility setting enabled, the requestAnimationFrame loop never starts, and a single static frame of snow is drawn instead — so the section still looks seasonally appropriate without a perpetual animation running for users who have asked for less motion.

Without it, the canvas backing store matches CSS pixels 1:1, and on a high-density screen the browser has to upscale the rendered result, producing visibly soft or blurry snowflakes. Sizing the backing store to clientWidth times the device pixel ratio and then calling ctx.setTransform(DPR, 0, 0, DPR, 0, 0) lets every drawing call keep using simple CSS-pixel coordinates while still rendering at full native resolution.

Move the setup into a mount effect: create the canvas ref, run resize() and seed() once mounted, start the requestAnimationFrame loop, and store the frame id so you can cancelAnimationFrame it on unmount. Re-attach the resize listener in the same effect and remove it in cleanup. The HTML and CSS structure — an absolutely positioned canvas inside a relative wrapper — ports unchanged.