Smooth Scroll

@sushindustries/ui

Mounts Lenis for the page and renders nothing. Respects reduced motion.

4 min read
View as MarkdownEdit on GitHub

Mount it once, at the root

SmoothScroll owns the whole document's scroll the moment it mounts - there is no scoping prop, because Lenis intercepts wheel and touch for the page, not for a subtree. Put it once near the top of the app. A second instance does not stack; it competes with the first for the same events.

The scroll veil

An iframe or a canvas swallows wheel events that pass over it, so a scroll gesture that crosses one loses its stream mid-flight - the flutter every page full of live previews had before this existed. While a scroll is in flight, SmoothScroll sets data-scrolling on <html>, and the stylesheet turns embedded surfaces pointer-events: none for exactly that long:

[data-scrolling] iframe,
[data-scrolling] canvas {
	pointer-events: none;
}
css

The gesture stays whole, and the previews are interactive again the instant the page settles - about 150ms after the last scroll event.

Opting an element out

Some elements need the browser's own scroll, not Lenis's: a table that scrolls sideways in its own frame, a code block, a drawer. Those carry data-lenis-prevent, which tells Lenis to leave that subtree alone entirely rather than steal its wheel and touch events. TableTableA table that is a table: declared columns, right-aligned numbers, sideways scroll in its own frame.@sushindustries/ui · content sets this attribute on its own frame for exactly this reason - drag inside a wide table and the table scrolls, not the page behind it.