Scrolling through pixels
One canvas, one number per cell, and a transition you control with your own hand.
Pixel Scroll Transition turns scroll progress into a wipe between two sections. This is the real component, in the page, and you’re already driving it. Scroll up and down over it.
That’s why it works as a transition. A wipe that plays on a timer happens to you. A wipe tied to your scroll is something you’re doing: slow down and it slows down, go back and it comes back.
Decide once, draw forever
Thousands of squares look like a lot of state. There’s almost none. Each cell holds a single number, the progress at which it turns on. Direction and pattern (random, checker, diagonal, wave, spiral, radial) only exist while that number is computed. Tick “Show the map” to see it: the darker a cell, the earlier it goes.
const dir = getDirectionalValue(r, c, rows, cols, direction);
const pat = getPatternValue(r, c, rows, cols, pattern, seed);
map[idx] = dir * (1 - patternIntensity) + pat * patternIntensity;After that, a frame is only a comparison. The random pattern is seeded rather than random, so the same cells go first every time: you can scroll back and find the page exactly as you left it.
const threshold = thresholdMap[idx] * endAt;
if (progress < threshold) continue;
const inAccentBand =
accentIndex >= 0 && progress - threshold < ACCENT_BAND_WIDTH;
ctx.fillStyle = inAccentBand ? accents[accentIndex] : colorB;
ctx.fillRect(xCoords[idx], yCoords[idx], wCoords[idx], hCoords[idx]);The color on the edge
A wipe from dark to light has a boring edge. The colored bands fix that with the same subtraction: a cell that turned on less than 8% of the scroll ago wears an accent color, then settles. There’s no timer and no animation, it’s a pure function of where you are. You get a lively edge that can’t go out of sync, whatever you do with the scrollbar.
The seams
Multiply 24 pixels by a device ratio of 1.5 and the squares stop landing on whole pixels. Hairlines appear between them, and they crawl while you scroll, which is the kind of thing people feel without being able to say what’s wrong. So every cell’s position and size are rounded once, against the real canvas, and kept in typed arrays.
const x = Math.round(c * stepDpr);
const xNext = Math.min(canvas.width, Math.round(x + pixelSizeDpr));
xCoords[idx] = x;
wCoords[idx] = xNext - x;The canvas is opaque too, which is one less thing for the browser to blend. Scroll events are collapsed into one draw per frame, and the theme gets watched: when the page’s colors change mid-transition, they’re read again and the current frame is redrawn.
Two ways to use it
Sticky pins a full screen and lets you scroll through the wipe, for moving between two chapters of a page. Inline, like the one above, is a block that plays as it crosses the viewport. Same maps, same draw: only what counts as progress changes.
<PixelScrollTransition mode="inline" pattern="wave" direction="left-right" pixelSize={28} />