Try it: metrix187.github.io/diffusion.css · Code: github.com/Metrix187/diffusion.css
Press the button and the stylesheet freezes a seed, turns it into noise, and denoises that into a 16×16 sprite in 12 steps. There is no <script> on the page. View source and check.
Why
People have already put a CPU, digit classifiers and, a few days ago, GPT-2 into CSS. I went looking for a diffusion model and couldn't find one, so this is my attempt.
The problem: CSS has no loops
Denoising is a loop. Each step needs the previous step's output, and a custom property can't refer to itself.
So the sampler is unrolled. Every step gets its own copy of the arithmetic, and every intermediate value gets its own registered custom property. A hidden unit looks like this:
@property --Ba0 { syntax: "<integer>"; inherits: true; initial-value: 0 }
/* first unit of the network used by step 2 */
--Ba0: max(0, (51096.5 + 25*var(--a108) - 26*var(--a123) - 31*var(--a126) /* 80 more terms */)/512);
Registering the property matters. An unregistered custom property is just text, so each var() would paste in the whole expression behind it and the stylesheet would blow up. A registered <integer> is computed once, rounded, and passed on as a number. There are 12,518 of them.
Seed and noise
The seed is a counter that a hidden checkbox pauses, so the moment you press is the random seed:
@keyframes spin { to { --seed: 15813250 } }
.app { animation: spin 211s linear infinite }
.app:has(#go:checked) { animation-play-state: paused }
Each of the 768 pixel channels then hashes that seed with mod() four times and sums the draws, which gives roughly Gaussian noise. That is the static you see before pressing.
The network rule only matches while the box is ticked, so nothing is computed until the seed is frozen. After that, one @keyframes rule points the 256 pixel elements at each sampler state in turn.
The model
Eleven small MLPs, one per noise level, trained in PyTorch on ten hand-drawn sprites that get recoloured to a random hue for every sample. Then they are pruned and rounded to integers: 202,863 weights, each one a literal inside a calc().
Two things that went wrong
Rounding. CSS rounds a calc() in an <integer> context to the nearest integer, but browsers don't agree on exact .5 ties. The fix: every division is by a power of two with 0.5 added to the numerator, so a quotient is never a tie. With that in place, a Python sampler using the same integer arithmetic produces identical pixels for the same seed. I checked 40 seeds in Chromium 152 and another 40-odd in Chrome 157.
Blended sprites. The first model produced a muddle of two sprites about a third of the time. Deterministic sampling with only 12 steps drifts between two shapes and never commits. Two changes fixed most of it: spacing the noise levels evenly in log-SNR, and training the later steps on inputs deliberately blended with a second sprite while keeping the dominant one as the target, so they learn to snap to one shape. It is down to about 1 in 20.
What it isn't
- Not a loop. All 12 steps are computed when the seed freezes. The animation reveals them; it does not drive the computation.
- Not creative. It learned ten shapes and a continuous range of colours. You get new shape-and-colour combinations, never a new shape.
- Not tested outside Chromium. It only uses features Firefox and Safari also ship, but I have not run it there.
Numbers
| Weights | 202,863 |
| Sampler steps | 12, deterministic DDIM |
| Registers | 12,518 |
| Possible seeds | 15,813,251 |
| File size | 5.2 MB, 0.8 MB gzipped |
| Time to compute a sprite | about 0.13 s |
Related reading
- 😱 Pure CSS! Neural Network / AI...it's easier than you think! 🤯 by GrahamTheDev, the earliest pure-CSS neural net post I found on DEV
- Implementing GPT-2 Entirely In CSS by Shubham Patil
- css-ml by mickael-btc, whose README describes the integer-register approach this builds on
- x86CSS by Lyra Rebane
The repo has the training code, the reference sampler and a console snippet for checking the page against it. If you get it running in Firefox or Safari, I would like to know whether the pixels match.

Top comments (0)