Skip to main content

Let It Snow

by jason

A holiday tradition, rebuilt from scratch as react-snow.

Since 2018, it has snowed at Moved most winters. Around the holidays, soft flakes drift down the screen, settle along the bottom, and melt away, and a little “Let it snow” switch turns them off for anyone who would rather work without weather. It is a tiny thing, but it is one of my favorite things I have shipped, and this year I finally rebuilt it properly.

The snow started as Scott Schiller’s DHTML Snowstorm, a script that has been “making it snow on the internets since 2003.” In December 2018 I forked it to fix its autostart, dropped it into Moved’s web app, and timed the first snowfall to begin with our holiday away message. It came back in the winter of 2021 with a switch to turn it off, and since December 2023 it has returned every year in our React admin, where the code that decides whether it is the season reads, one variable at a time, Let, it, snow.

This fall I rewrote it from the ground up as react-snow, a small snow library for any web page with a React component on top. It isn’t a picture of snow; it’s snowing right here:

react-snow, live. Move your pointer across it to steer the wind; turn off “Let it snow” and the flakes in the air land and melt.

Why start over

Snowstorm did its job for years, but it comes from a very different web. It is one global object configured by changing its properties, it starts itself when the page loads, and it draws each flake as its own little HTML element, moving every one of them a fixed step on every frame. About half of the script is scaffolding for Internet Explorer 6 and old browser prefixes. It can’t run twice on a page, it never cleans up after itself, and because each frame moves a flake the same distance, snow falls faster on a screen that draws more frames per second.

In a modern React app that meant loading a script by hand, waiting for its global to appear, and poking at it from outside. I wanted snow that behaves like any other component: put it where you want it, give it options, and take it away cleanly.

How it works

One canvas. Instead of a page element per flake, react-snow draws every flake onto a single canvas. Each look is drawn once ahead of time, and every frame simply stamps those copies in their new positions, so hundreds of flakes are cheap.

Time, not frames. Every flake moves by how much time has passed since the last frame, so the snow falls at the same speed on a 60Hz laptop and a 120Hz phone.

A sense of depth. Size, brightness, and speed are linked: small flakes are dimmer and slower, so they read as far away, and the big bright ones seem close. Each one also sways on its own gentle rhythm, drifts with the wind, rests along the bottom edge for a few seconds, and shrinks away as it melts.

Knowing when to rest. The animation stops itself whenever nothing can change: when it’s paused, scrolled offscreen, in a hidden tab, or out of flakes. The default amount, half a flake for every 100 by 100 pixels, matches the 64 flakes Snowstorm put on a typical laptop screen.

Releases install straight from GitHub, and using it in React is one line:

// pnpm add github:jasondecamp/react-snow#semver:^0.2.0
import { Snow } from 'react-snow';

<Snow falling={snowing} density={0.75} followPointer />

Flakes can be soft dots, any SVG shape, or the silhouette of a PNG, tinted by color and depth, and they can spin as they fall. Every option has a control in the playground, which writes out the matching code as you go.

Built with Claude Code

Like Ethereal, react-snow was built entirely with Claude Code; I never touched the code by hand. It started with Claude interviewing me, one question at a time, about every decision: what to keep from Snowstorm, what to rename, what to drop, how fast is fast. The answers became a plan, and the plan became the library. The first release, with its tests, playground, and automated releases, and the second, with custom flake shapes, spinning, and the public playground, both shipped the same afternoon.

As with Ethereal, the part only I could do was look at it. Claude can reason about fall speeds in pixels per second, but it can’t watch snow fall and feel whether it’s peaceful or frantic. That is why the playground exists: a control for every option, a day and a night sky to check how flakes read against each, and a live snippet of the settings, so a look I liked could go straight into a decision. Custom image flakes, for example, were first drawn exactly as given. That felt strange in the playground: dots and SVG shapes took on the snow’s color, but the test PNG, a simple white snowflake, stayed white and vanished against the day sky. So PNG flakes changed to use just the image’s shape, filled with the snow’s color and blended by distance like every other flake.

What it accomplished

It works, and it is a much better neighbor than the snow it replaces: one canvas instead of hundreds of elements, the same speed on every screen, a clean exit, and as many storms on a page as you like. Moved’s admin has already switched to it, ready for this winter.

It is also not finished. Version 0.2 is honest about that: the defaults are still being tuned by eye in the playground, and gusts of wind that sweep across the whole storm are next. Once the defaults feel right it will be 1.0. It respects reduced motion by slowing everything down, but deciding whether to show snow at all is left to each app, and in the end it is a decoration. Nobody needs snow on their screen.

But every winter somebody at Moved notices it for the first time and smiles, and that is the whole point. Some of the best things on the internet are the ones that are just there for the joy of it.