Pixelated Marquee
An infinite logo/content scroller that dissolves into pixels at the edges using an ordered Bayer dither mask — instead of the usual soft fade.
Usage
import { PixelatedMarquee } from "~/components/marquee/pixelated-marquee";
<PixelatedMarquee duration={32} edgeWidth={120} pixelSize={8}>
{logos.map((logo) => (
<Logo key={logo.id} {...logo} />
))}
</PixelatedMarquee>How the edge works
A normal marquee fades its edges with a linear-gradient mask. This one instead carves the edge into discrete pixel blocks using an ordered (Bayer) dither — the same trick old games used to fake transparency on hardware that only had on/off pixels.
For each cell along the edge, its horizontal progress toward the solid interior is compared to a threshold from a 4×4 Bayer matrix. If progress beats the threshold, the pixel stays; otherwise it's cut. The result is a crisp, retro dissolve that's rendered entirely with a CSS mask — no canvas, no JS per frame.
const BAYER_4X4 = [
[0, 8, 2, 10],
[12, 4, 14, 6],
[3, 11, 1, 9],
[15, 7, 13, 5],
];
// A cell is revealed when its distance from the edge
// exceeds its Bayer threshold -> crisp pixel dissolve.
const threshold = (BAYER_4X4[row][col % 4] + 0.5) / 16;
if (progress > threshold) drawPixel(col, row);The mask is built once as an SVG data URI and tiled vertically, so the effect is essentially free at runtime and respects prefers-reduced-motion.
Props
| prop | type | default | description |
|---|---|---|---|
| children | ReactNode | — | Items to scroll. Rendered twice for a seamless loop. |
| duration | number | 32 | Loop duration in seconds. Lower is faster. |
| direction | "left" | "right" | "left" | Scroll direction. |
| gap | number | 56 | Gap between items, in px. |
| edgeWidth | number | 112 | Width of the dissolving edge region, in px. |
| pixelSize | number | 8 | Size of each dissolve pixel block, in px. |
| pauseOnHover | boolean | true | Pause the scroll while hovered. |