dither in the browser with the power of CSS
posted on /en/css
A while back Surma wrote this great post about image dithering. I got from there to the linked forum post about the dithering in Return of the Obra Dinn, and also found Bart Wronski’s nice comparison of multi-step quantization. I’ve been playing around with CSS and retro vibes lately, and something about the simple descriptions of dithering just itched at me. It seemed, I thought, like it really ought to be doable with CSS, even if that still had trying-to-parse-html-with-regex vibes.
tl;dr it was, in fact, doable
Here is a selfie I took on a day when my hair was really good.

Wow, good hair. Sorry, you’re gonna have to look up my nose a bit for the rest of this.

Right-click on that image, open it in a new tab, and you will see that it is the same image. Voila! Dithered in the browser at the size desired.
So how do we get from A to B?
It’s easy enough to fake a 1-bit conversion with a threshold; we just desaturate the image and up the contrast.
filter: saturate(0) contrast(250);
But dithering means we’re going to need to introduce some noise to the picture before we do that, so we’re going to need to introduce some structure to our markup1.
Google “blue noise tile” and find one that seems sensibly sized for your purposes. If you find one that is partially transparent, you’ll be able to use it without modification; if you don’t (more common) you can fuss with CSS blending modes to make it work, but I won’t cover that part here.
Your markup will need to look something like:
<div class="zalgodither">
<picture>
<img alt="good hair selfie, dithered" src="/assets/content/selfie.jpg">
</picture>
</div>
Now we introduce a pseudoelement in the CSS for the noise overlay:
.zalgodither > picture::after {
content: '';
width: 100%;
height: 100%;
pointer-events: none;
position: fixed;
display: block;
z-index: 2;
top: 0px;
left: 0px;
background-image: url(< YOUR BLUE NOISE ASSET GOES HERE >);
background-repeat: repeat;
}

Now we’re going to add that thresholding filter back on the wrapper div to apply over top of this.

Whoa! It’s too noisy, right? You’re going to want to play with the opacity of the noise overlay. I picked 40% for this, but it’ll look best chosen individually for each photo. (You can also mess with the brightness of the picture with CSS for different results, but we won’t get into that here.)
There’s only one more step for the best dither:
image-rendering: pixelated;
😎
The cruise control for cool of our times.

My next step is going to be trying this same thing but with a Bayer matrix. (Update: see that here)
Let me know if you use this for anything fun!
n.b. You should probably mostly use this for experimentation, not Your Whole Site. imagemagick
makes it really easy to dither images on the command line if you know you want dithering, and then your asset sizes will be friendly and small. There are reasons why you might do this instead that I’m sure you can think of, but don’t discount the power and flexibility of Real Dithering, not cursed CSS dithering.
-
I did try for minimal markup here, but specs fought me. ↩