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

Snow Animation CSS: 9 Snowfall Scenes in Canvas

Snow animation css: Why do big snowflakes look faster and sharper? 01 reads size as depth for speed and blur, 03 keeps piles under the slope where snow slides,

Snow Is a Particle with Depth, Not a Falling Dot: Snow Animation CSS Starts by Letting One Size Set Speed, Blur and Sway

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

All nine scenes use the same flake particle and differ only in how its values are read. 01 and 02 decide how snow falls, 03, 05 and 07 decide where a landed flake stays, and 04 and 06 turn snow and light into a texture. 08 trades speed for streaks that close the view, and 09 stacks several of those calculations in one frame.

Why big flakes fall fast and sharp

In 01 the flake radius s runs between S_MIN 1.1 and S_MAX 5.2 and becomes a depth z from 0 to 1. That single z sets the fall speed (0.6 to 1.4 times), the blur radius (up to 3.4px) and the sideways sway width (4.5 to 19.5px). The biggest flake therefore drops about 2.3 times faster than the smallest, and the sense of front and back appears without picking values per flake.

A snow animation css codepen is handy, but this file needs only a canvas

A snow animation css codepen is a good place to try a quick idea, and the files here go one step further because each scene keeps its position state in plain script. The nine scenes use only the 2D canvas and requestAnimationFrame, with no library, WebGL or CDN. The random numbers come from a seeded generator, so the same code replays the same snow, and one frame is capped at DT_MAX 0.05 seconds.

Where this differs from the neighboring posts

Floating dots as decoration belong to the particle background post, and rain that bounces off the ground is in the rain effect post. Here snow stays where it lands: it builds on a height map, wets glass and slides down an umbrella. Weather shown as icons sits in the weather widget post, and the baseline for canvas scenes like these is the canvas scene effects post.

Snow Animation CSS: 9 Scenes — Snowflake Layers That Read Size as Depth, Snow Swirled by a Gust Field, Piles That Build and Slump, Snow That Melts on Window Glass, a Snow Globe That Is Shaken and Settles, Snow in a Lamp's Light Cone, Snow Sliding off an Umbrella, Blizzard Stretched into Streaks and a Snowy Night Rooftop Hero Scene

01 and 02 change the math of the fall, while 03 to 05 and 07 store a state after the flake lands. 06 changes only brightness and leaves the motion alone. 08 stretches the same flakes along their velocity, and 09 raises the pile of 03, the lamp of 06 and the depth of 01 together.

What a 2-second capture of the 480×300 stage measured

Each scene was rendered for 2 seconds at 480×300 and 24 frames were captured, which leaves 23 neighbor differences to count.

Scene Area covered Frames that moved Mean intensity
01 Depth-layered flakes 46.6% 23/23 48.3
02 Gust field 41.8% 23/23 59.7
03 Piles that slump 27.2% 23/23 85.1
04 Melting on glass 21.6% 23/23 29.4
05 Snow globe 20.5% 23/23 81.0
06 Lamp cone 48.4% 23/23 42.0
07 Umbrella slide 24.0% 23/23 57.4
08 Blizzard 70.2% 23/23 46.8
09 Rooftop hero 27.7% 23/23 46.2

The streaks and white veil of 08 spread over the whole stage, so it covers the most at 70.2%, and 05 covers the least at 20.5% because snow moves only inside the globe. Mean intensity peaks at 85.1 in 03, where piles rise in two panels, and bottoms out at 29.4 in 04, where only moisture on glass changes. All nine scenes moved in 23 of 23 frames.

01Snowflake Layers That Read Size as Depth

Put it behind the first screen of a winter event landing page and 240 flakes fall in front and back layers. speedK stretches depth 0 to 1 into a speed multiplier of 0.6 to 1.4, and blurOf blurs far flakes more. The flakes are sorted by size once, so near ones are drawn on top.

Snowflake Layers That Read Size as Depth
Open in a new tab

01 · Snowflake Layers That Read Size as Depth — Measured area 46.611% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Z_SPREADS_MAXspeedK
    function depthOf(s) { return (s - S_MIN) / (S_MAX - S_MIN); }
    // speed multiplier from depth: 0.6 to 1.4 when Z_SPREAD is 0.5, 0.2 to 1.8 when it is 1.0
    function speedK(z) { return lerp(1 - 0.8 * Z_SPREAD, 1 + 0.8 * Z_SPREAD, z); }
    function blurOf(z) { return (1 - z) * BLUR * (0.5 + Z_SPREAD); }
    function fallOf(f) { return FALL * speedK(f.z); }
    function reset() {
        rng = seeded(101);
        flakes = [];
        for (let i = 0; i < FLAKES; i += 1) {
            const s = S_MIN + Math.pow(rng(), 1.7) * (S_MAX - S_MIN);
            flakes.push({ bx: rng() * (W + 60) - 30, y: rng() * (H + 20) - 10, s: s, z: depthOf(s), ph: rng() * TAU });
        }
        // draw far ones first: sort by size once so near flakes land on top
        flakes.sort(function (a, b) { return a.s - b.s; });
    }

02Snow Swirled by a Gust Field

Behind a seasonal event popup, snow falls in a line, curls up at a gust and sinks again. The gradient of three summed sines is turned 90 degrees and used as velocity, so snow neither collapses to a point nor only spreads, it circles. Shallow flakes take only 0.3 to 1.0 of the field and curl less.

Snow Swirled by a Gust Field
Open in a new tab

02 · Snow Swirled by a Gust Field — Measured area 41.808% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GUSTGUST_SPEEDfieldAt
    function fieldAt(x, y, t) {
        let u = 0;
        let v = 0;
        for (const w of WAVES) {
            const c = Math.cos(x * w[0] + y * w[1] + t * w[2] + w[3]) * w[4];
            u += c * w[1];
            v -= c * w[0];
        }
        return [u * 34, v * 34];
    }
    // gust crest: a share that swells and fades over time (0.3 to 1)
    function gustAmp(t) { return 0.65 + 0.35 * Math.sin(t * 1.4); }

03Piles That Build and Slump

Use it for the snow-capped text ledge of a year-end greeting page. A landed flake adds 3px to a per-column height map, and a column higher than its neighbor by more than the REPOSE limit sends half of the excess downhill. The left panel is the broken copy with the slope limit switched off, and the right panel is the repaired one that keeps the limit.

Piles That Build and Slump
Open in a new tab

03 · Piles That Build and Slump — Measured area 27.164% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

REPOSEGRAINrelax
    function relax(p) {
        if (p.limit >= 1e6) {
            return;
        }
        const maxDiff = p.limit * GRAIN;
        for (let pass = 0; pass < 4; pass += 1) {
            for (let c = 0; c < COLS; c += 1) {
                for (let d = -1; d <= 1; d += 2) {
                    const n = c + d;
                    if (n < 0 || n >= COLS) {
                        continue;
                    }
                    const excess = surface(p, c) - surface(p, n) - maxDiff;
                    if (excess > 0 && p.h[c] > 0) {
                        const move = Math.min(p.h[c], excess * 0.5);
                        p.h[c] -= move;
                        p.h[n] += move;
                    }
                }
            }
        }
    }

04Snow That Melts on Window Glass

It suits the window-side background of a cafe page. A touching flake raises the moisture of a 4px cell, and the drop then runs downward, leaving moisture in each cell and losing mass. Moisture dries by EVAP, so a trail runs long and then fades, and a fog layer is punched open only where the glass is wet to show the light beyond.

Snow That Melts on Window Glass
Open in a new tab

04 · Snow That Melts on Window Glass — Measured area 21.645% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

EVAPTRAIL_WETLAND_PER_S
        // spread along gravity: a heavier drop runs faster, wets the cells it passes and loses mass
        for (const d of drops) {
            if (d.wait > 0) {
                d.wait -= dt;
                continue;
            }
            const v = 14 + 70 * d.m;
            const steps = Math.max(1, Math.round(v * dt / CELL));
            for (let s = 0; s < steps; s += 1) {
                d.y += v * dt / steps;
                d.x += Math.sin(d.y * 0.09 + d.m * 9) * 0.12;
                const c = clamp(Math.floor(d.x / CELL), 0, GC - 1);
                const rr = Math.floor(d.y / CELL);
                if (rr >= 0 && rr < GR) {
                    const i = rr * GC + c;
                    water[i] = Math.min(1, Math.max(water[i], TRAIL_WET));
                    d.m -= TRAIL_COST;
                }
            }
        }

05A Snow Globe That Is Shaken and Settles

Use it as a decorative card on a gift shop page. On a fixed schedule (0.25 and 3.6 seconds) the piled snow returns to particles and takes a jolt, and drag bleeds the speed down to about 49.6px per second. A particle that comes within FLOOR_SLACK 4px of the floor at a speed under 40 is added to the floor pile by PILE_ADD 1.5px, so after a shake the snow settles back into a pile.

A Snow Globe That Is Shaken and Settles
Open in a new tab

05 · A Snow Globe That Is Shaken and Settles — Measured area 20.46% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYSHAKE_ATRESTITUTION
            // air drag: cut the speed in proportion to itself
            const drag = Math.exp(-DRAG * dt);
            g.vx *= drag;
            g.vy *= drag;
            g.x += g.vx * dt;
            g.y += g.vy * dt;
            const dx = g.x - CX;
            const dy = g.y - CY;
            const d = Math.hypot(dx, dy);
            const lim = R - 5 - g.s;
            if (d > lim) {
                // wall bounce: flip only the outward normal part of the velocity and keep RESTITUTION of it
                const nx = dx / d;
                const ny = dy / d;
                const vn = g.vx * nx + g.vy * ny;
                if (vn > 0) {
                    g.vx -= (1 + RESTITUTION) * vn * nx;
                    g.vy -= (1 + RESTITUTION) * vn * ny;
                }
                g.x = CX + nx * lim;
                g.y = CY + ny * lim;
            }

06Snow in a Lamp's Light Cone

It fits the hero background of a night-mode service page. Each flake's brightness is its cone share times distance falloff times forward scattering, and outside the cone only AMBIENT 0.16 is left, so the rest stays dark. The cone axis sweeps left and right on a sine, which keeps swapping which flakes glow.

Snow in a Lamp's Light Cone
Open in a new tab

06 · Snow in a Lamp's Light Cone — Measured area 48.378% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CONE_HALFLAMP_RANGEPHASE_G
    // brightness of one flake: cone share x distance falloff x (1 + forward scatter); outside the cone it is AMBIENT
    function brightness(f, axis) {
        const dx = f.x - LAMP_X;
        const dy = f.y - LAMP_Y;
        const d = Math.hypot(dx, dy);
        if (d < 1) {
            return 1;
        }
        let th = Math.abs(Math.atan2(dy, dx) - axis);
        if (th > Math.PI) {
            th = TAU - th;
        }
        const t = th / CONE_HALF;
        const cone = t < 1 ? Math.pow(Math.cos(t * Math.PI / 2), CONE_POWER) : 0;
        const fall = 1 / (1 + (d / LAMP_RANGE) * (d / LAMP_RANGE));
        // forward scatter: how much light passes the flake toward the camera. Larger for near flakes (big z), zero at the cone center and outside, strongest near half the half-angle
        const toward = clamp(f.z * 1.2 - 0.2, 0, 1) * Math.sin(Math.min(1, t) * Math.PI);
        const cosA = clamp(toward, 0, 1);
        const hg = (1 - PHASE_G * PHASE_G) / Math.pow(1 + PHASE_G * PHASE_G - 2 * PHASE_G * cosA, 1.5);
        const scat = SCATTER * hg * 0.14;
        return clamp(AMBIENT + cone * fall * (0.9 + scat), 0, 1.4);
    }

07Snow Sliding off an Umbrella

Place it in the snowy-day card of a weather alert service. The normal of the elliptical canopy is found, only flakes that come from above and cross the surface touch it, and 0.6 of the tangent speed is kept so they slide to the rim. Flakes blown in sideways pass under the canopy, and the oval below it is only a shadow that blocks no snow, while flakes that reach the ground add to a pile capped at CAP 16px.

Snow Sliding off an Umbrella
Open in a new tab

07 · Snow Sliding off an Umbrella — Measured area 23.972% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SLIDE_GFRICTIONnormalAt
                if (Math.abs(f.x - CX) < A) {
                    const sy = surfY(f.x);
                    // only a flake coming from above that crosses the surface lands; a sideways flake passes under the canopy
                    if (prevY <= sy && f.y >= sy) {
                        const n = normalAt(f.x, sy);
                        f.dir = f.x >= CX ? 1 : -1;
                        // tangent: take the downhill side (the one pointing down)
                        const tx = f.dir * -n[1];
                        const ty = f.dir * n[0];
                        // drop the normal part and keep 0.6 of the tangent part as speed
                        f.speed = Math.max(0, (f.vx + wind) * tx + f.vy * ty) * 0.6;
                        f.y = sy;
                        f.mode = 1;
                    }
                }

08Blizzard Stretched into Streaks

Use it as the loading scene of a winter sports service page. Each flake is drawn as a segment along its velocity with length speed times EXPOSURE, and far flakes are mixed into the sky color to cut visibility. At the gust peak the white veil rises to 0.34 opacity and the stage whites out.

Blizzard Stretched into Streaks
Open in a new tab

08 · Blizzard Stretched into Streaks — Measured area 70.178% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

EXPOSUREHIDDEN_SDT_MAX
    function drawStorm(s, ox) {
        ctx.save();
        ctx.beginPath();
        ctx.rect(ox, 0, PANEL_W, H);
        ctx.clip();
        ctx.translate(ox, 0);
        const g = gustAt(sceneT);
        const sky = mix(SKY_TOP, SKY_BOT, 0.5);
        for (const f of s.fl) {
            const vx = (WIND * g + 40) * (0.4 + f.z);
            const vy = FALL * (0.3 + 0.9 * f.z);
            const len = EXPOSURE;
            const c = mix(SNOW, sky, (1 - f.z) * FOG);
            seg(f.x - vx * len, f.y - vy * len, f.x, f.y, 0.7 + f.z * 1.9, c, 0.3 + 0.65 * f.z);
        }
        // whiteout: a veil that thickens as the gust strengthens
        ctx.fillStyle = rgba(SNOW, 0.34 * g * g);
        ctx.fillRect(0, 0, PANEL_W, H);
        ctx.restore();
    }

09Snowy Night Rooftop Hero Scene

Use it as the hero of a year-end campaign site. Only near flakes (z of 0.45 or more) pile on the roof-shaped height map, and each one is split over three columns as 0.5, 0.25 and 0.25 so the roofline thickens smoothly. Only flakes inside the lamp cone are bright, and far flakes pass behind the house and return upward from the ground.

Snowy Night Rooftop Hero Scene
Open in a new tab

09 · Snowy Night Rooftop Hero Scene — Measured area 27.677% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ROOF_SLOPEREPOSECONE_HALF
            // only near flakes (z >= 0.45) reach the roof and ground and pile up; far flakes pass behind the house and loop back from the bottom
            if (f.z >= 0.45) {
                const c = clamp(Math.floor(f.x / GRAIN), 0, COLS - 1);
                if (f.y >= topOf(c)) {
                    h[c] += ADD * 0.5;
                    if (c > 0) {
                        h[c - 1] += ADD * 0.25;
                    }
                    if (c < COLS - 1) {
                        h[c + 1] += ADD * 0.25;
                    }
                    f.y = -4 - rng() * 20;
                    f.x = rng() * (W + 40) - 20;
                }
            }

When Building a Snow Accumulation Scene, Where and How Much Snow Should You Add to Look Natural?

Snow does not pile evenly everywhere. In 03 about 65% of the flakes are dropped near three stream columns (17, 40 and 64) and the rest are scattered across the stage, so the snow is thick under a stream and thin between them.

Piling on a text ledge

If you set shelves of different heights as BASE, such as the top faces of letters, snow stops on the surface made of shelf plus pile. The shelves in 03 are three blocks of 18px, 36px and 12px, and snow starts to build on the top of each. For a greeting page, swap them for the heights of the title letters.

Piling on a roof

09 lets only near flakes pile and sends far ones back from the ground behind the house. A sudden floor step of more than 6px, like a chimney, is treated as a wall and not slid, so snow stays beside the chimney and bends the line there. With ADD at 4, the roofline is thicker at the end of the 7-second loop.

Surfaces where snow does not pile

04 shows the amount of snow as melted moisture on glass, and 07 lets it slide off the canopy. The same snow leaves different traces depending on the surface it touches, and that difference is what makes a snow scene convincing.

Build Snow on a Height Map, Let It Slide Sideways Past the Slope, Shake It with Wind: The Order in Which One Flake Becomes a Pile

One flake becomes a pile in four steps. Add the landed flake to a height map instead of deleting it, move any difference past the limit sideways, shake with wind, and draw far flakes first.

Step What it does Names used Scenes
1 Add Add ADD to the column height and send the flake back from the top surface, stepPanel 03, 07, 09
2 Slide If a column is higher than its neighbor past the limit, move half of the excess downhill relax 03, 09
3 Shake Add a summed-sine field or the arrow-key value to the flake velocity fieldAt, WIND_STEP 01, 02, 05, 08
4 Draw Sort by size and draw far flakes first flakes.sort 01, 02

Why only the difference past the limit moves

03 sets the allowed height gap to REPOSE times GRAIN. At 1 times 3, only a column more than 3px above its neighbor slumps, and with 3px columns the slope is 45 degrees. Gaps under the limit are left alone, so the pile hardens into a gentle hill.

Why the slide repeats four times

When one column slumps and raises a lower neighbor, that neighbor can pass the limit in turn. 03 loops 4 times inside relax per step and 09 loops 3 times, so a slump can spread across a few columns. More passes smooth the pile but cost more with every column.

Why far flakes are drawn first

01 and 02 sort the flakes by size once and draw in that order. Small flakes are the far ones, so a big flake covering them looks natural, and sorting once costs nothing per frame.

Where Snow Breaks: Flakes That Vanish All at Once at the Bottom, Piles That Stand Up Like Towers and Snow That Pours Down After a Hidden Tab

Deleting at the bottom leaves a gap

Deleting flakes that leave the bottom of the screen makes the snow disappear at one moment and leaves a gap until the top refills. 03 adds the landed flake to the height map and sends it back from the top, so the flake count stays steady.

    function stepPanel(p, dt) {
        const wind = userWind() * 30;
        for (const f of p.fl) {
            f.y += f.v * dt;
            f.x += wind * dt;
            const c = clamp(Math.floor(f.x / GRAIN), 0, COLS - 1);
            if (f.y >= GROUND_Y - surface(p, c)) {
                // a landed flake does not vanish: it is added to the height map and comes back from the top
                p.h[c] += ADD;
                respawn(p, f);
            }
        }
        relax(p);
    }

The panel where piles stand like towers

The left panel of 03 sets limit to 1e9, so relax returns at once, and that is the broken copy. Snow lands on only three stream columns, so those columns rise as towers with no regard for neighbors. The right panel slides the same snow at REPOSE 1 and ends as a gentle hill.

Snow that pours down after a hidden tab

The left panel of 08 spends the 1.3 seconds of a returned hidden tab in one step at each time on the schedule (0.3, 1.6, 2.9 and 4.2 seconds). Since the number of new flakes is proportional to RATE times dt, 190 times 1.3 gives about 247 flakes pouring from the top at once, while the right panel, cut to DT_MAX 0.05 seconds, gets only about 10.

    function update(dt) {
        if (hiddenSeen < HIDDEN_AT.length && sceneT >= HIDDEN_AT[hiddenSeen]) {
            hiddenSeen += 1;
            // the left panel takes the long frame as is, and the right takes only what DT_MAX allows
            stepStorm(storms[0], HIDDEN_S);
            stepStorm(storms[1], Math.min(HIDDEN_S, DT_MAX));
            return;
        }
        for (const s of storms) {
            stepStorm(s, dt);
        }
    }

When a real tab hides, the loop of the nine scenes stops, and on return lastTs is set back to -1 so the hidden time is not charged to the first frame. DT_MAX is applied once more in case a long frame still arrives. Unzip the archive with the password egxx7s4z, then pause the index.html files of the 03 and 08 folders to see where the broken and repaired copies split.

Strengthen the Gusts and How Far Does the Snow Swirl? Gust Strength, Depth Range and Pile Speed Compared

Name Changed value Feel
Stronger gusts 02 GUST 0.6 → 1.5 The field velocity factor added to each flake goes from 0.6×200=120 to 1.5×200=300px/s, 2.5 times, so snow curls up in bigger loops and straight falls break apart sooner
A steeper slope limit 03 REPOSE 1 → 3 The height gap allowed to a neighbor grows from 3px to 9px, so the slope steepens from 45 degrees to about 72, piles stand pointier and taller, and slumps become rarer
A wider depth range 01 Z_SPREAD 0.5 → 1.0 The speed multiplier widens from 0.6 to 1.4 times to 0.2 to 1.8 times, and the blur radius of the farthest flake grows from 3.4px to 5.1px, deepening the sense of front and back

Numbers to check before changing a value

GUST in 02 is a velocity factor, not a length. At 1.5 the field velocity reaches 300px/s, but the gust crest share (0.3 to 1.0) and the depth share (0.3 to 1.0) still multiply it, so the real curl is below that. Raising REPOSE to 3 moves the right panel of 03 toward the broken left one, so keep the two side by side when you compare.

One Snowy Scene Under Reduced Motion: Accessibility Through a Sentence Describing the Snowfall and a Pause Button

Arrow keys in place of a hand

The stage box of each scene takes focus, and each left or right arrow press moves the wind by WIND_STEP 0.25 within -1 to 1, while a horizontal drag makes the same wind. Enter and Space press pause, and the first time a person touches the stage the auto wind lets go and the status line switches to aria-live polite. In 03 that line reads out the current state as a sentence, one height for the unlimited pile and one for the limited pile.

Reduced motion keeps one snowy scene

With prefers-reduced-motion: reduce, the loop stops and only one still scene computed for REST_STEPS steps is drawn. Falling, gusting and piling do not move, and only the look of snow already fallen remains. If you change the setting with the page open, the change listener reads it again and switches the scene. When you paste this into a page, copy the branch that reads the setting along with it. The spec is in MDN on prefers-reduced-motion.

Snow Animation CSS FAQ: CSS-Only Snow, Load on Phones and Using It as a Background

Can the same scenes be made with a snowfall animation css alone

For falling by size as in 01, a css animation can do it, but the scenes in this post store and compute a position for each flake. The height map, the umbrella slide and the lamp brightness read values and redraw every frame, so canvas and a snowfall effect javascript loop fit better.

How many flakes can a phone handle

The FLAKES constant in each scene is its flake count, 240 in 01 and 300 in 06. The canvas interior is fixed at 480×300 and devicePixelRatio is capped at 2, so the work stays the same when the screen grows. If it still runs slowly, try halving FLAKES first.

Can I use it as a page background

Decorative scenes such as 01, 06 and 09 are good for hero backgrounds, and the loop stops when the tab hides. Where text must be read, lower FLAKES or put one translucent panel over the snow to keep text contrast.

Enter the archive password

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