Retro shading is a rendering decision.
Maxime Heckel's long-form guide treats dithering, color quantization, pixelization, and CRT-style cells as separate parts of a post-processing stack. That separation matters. A limited palette creates visible bands. Dithering redistributes the error between those bands. Pixel scaling decides whether the pattern stays sharp when the browser enlarges it.[1]
The effect works because neighboring marks can suggest an intermediate tone that the palette does not contain. On an old display, that solved a hardware limit. On a current website, it is a style choice. The constraint needs a job.
A dither pattern should reveal the shape. If it competes with the label, it is overspray.
Keep the browser's two scaling controls straight.
CSS image-rendering selects how a scaled image is sampled. MDN notes that it does nothing when the image is not scaled. crisp-edges preserves contrast with an algorithm such as nearest neighbor. pixelated uses nearest-neighbor-style scaling to the nearest integer multiple, then smooths to the final size when needed.[2]
Canvas has its own switch. imageSmoothingEnabled is true by default. Set it to false before enlarging a low-resolution canvas when you need hard pixel edges.[3]
Neither setting creates a dither. The pattern must already exist in the source pixels or be produced by your drawing code or shader. Scaling controls only decide how those pixels travel to the screen.
Ordered noise is easier to art-direct.
An ordered Bayer matrix repeats a fixed threshold pattern. The repetition is visible, which is useful when the page wants a mechanical print texture. Random noise breaks the grid but can shimmer if it changes while the content moves. Keep the random field stable between frames unless movement is the intended effect.
Start with four levels per color channel and a 4-by-4 ordered matrix. Increase the palette before shrinking the pattern. A larger palette reduces the pattern's workload. A smaller pixel block reduces its visual weight. Test both on the real background instead of tuning against a checkerboard forever.
ctx.imageSmoothingEnabled = false;
const threshold = (bayer4[y & 3][x & 3] + 0.5) / 16 - 0.5;
const adjusted = channel + threshold * 255 / (levels - 1);
const quantized = Math.round(adjusted / 255 * (levels - 1))
* 255 / (levels - 1);
Reserve clean lanes for reading.
Do not run full-strength dithering under body copy, small controls, focus rings, or error text. Keep those surfaces solid. Use the treatment on illustrations, large display type, transitions, or bounded panels where its texture has a clear role.
The W3C CSS Images draft defines image-rendering as a scaling preference for images that need to be resized. It also notes that the user agent may choose algorithms that balance speed, quality, and other factors. Treat browser output as something to inspect, not as a promise of identical pixels everywhere.[4]
Ship the plain image first. Add the treatment as a layer. Keep a reduced-motion path if parameters animate. Then look at the result on a dense desktop screen and a narrow phone. Retro is not permission to make the interface harder to read.