Building a pixel-dissolve marquee
Every logo marquee on the internet fades its edges the same way: a
linear-gradient mask that smoothly ramps opacity to zero. It's fine. It's also
everywhere. I wanted the edge to feel like something — so I made the content
dissolve into pixels instead.
The trick: ordered dithering
Old hardware couldn't do partial transparency — a pixel was on or off. To fake gradients, artists used ordered dithering: a fixed threshold matrix that decides, per pixel, whether it's on or off. The classic is the 4×4 Bayer matrix.
const BAYER_4X4 = [
[0, 8, 2, 10],
[12, 4, 14, 6],
[3, 11, 1, 9],
[15, 7, 13, 5],
];The numbers are thresholds. For each pixel cell along the edge, I compute how
far it is toward the solid interior (progress, from 0 to 1) and compare it to
its threshold. Beat the threshold, the pixel survives. Otherwise it's cut.
const threshold = (BAYER_4X4[row][col % 4] + 0.5) / 16;
if (progress > threshold) keepPixel(col, row);Because the matrix is spatially distributed, you get a clean gradient of density — lots of pixels near the interior, almost none at the outer edge.
It's all a CSS mask
The clever part: none of this runs per frame. I render the dither into a tiny
SVG, encode it as a data URI, and hand it to CSS as a mask-image. Three mask
layers composite together — a dithered left edge, a solid center, and a
dithered right edge:
const image = `${leftDither}, ${centerSolid}, ${rightDither}`;
style.maskImage = image;
style.maskComposite = "add"; // union of the threeThe SVG tile is one Bayer period tall and repeats vertically. The browser does the compositing on the GPU, so the marquee animation stays buttery and the effect costs essentially nothing.
Takeaways
- A
maskdoesn't have to be a smooth gradient. Anything with an alpha channel works — including a hand-built dither pattern. - Building the mask once as a data URI keeps the runtime cost at zero.
- Always gate the scroll behind
prefers-reduced-motion.
The full, prop-driven component lives on the
Pixelated Marquee page — drag the sliders to
feel how pixelSize and edgeWidth change the dissolve.