Skip to content
← writing

Building a pixel-dissolve marquee

css
masks
dithering
frontend

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.

Orbit
Prism
Vertex
Loop
Quanta
Flux
Cobalt
Ember

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 three

The 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 mask doesn'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.