We removed Lenis and ScrollTrigger: scrolling is now CSS.
Two scrolling libraries retired and no third took their place; the scenes moved onto the platform’s own timeline — and both the gain and the cost were measured.
On this site desktop scrolling was run by one library and the scenes were triggered by a second one. Both are gone. No third library replaced them: scrolling is now the browser’s own timeline, and the scenes are CSS animations running on it. This post is the measurement of that move — what it won and what it cost.
Hypothesis
The hypothesis was this: every scroll scene on this site — the pins, the focus gesture, the typewriter, the counters, the showcase parallax, the reading line — can be built on the platform’s own view timeline. JavaScript is left with what CSS cannot do: canvas, data, accessibility and text. In exchange, the bytes of two libraries and a sync loop running on every frame go away.
Method
Four slices, each closed with its own full test run, and none of them carried the previous one’s red. First the four bindings of the smooth-scrolling library were handed over one at a time: the trigger sync, the anchor sync, the in-page glide, and the marquee’s direction and speed. Then one-shot entrances moved to an intersection observer and the first two surfaces to CSS timelines. The third slice moved the scenes of the home page and the service pages. In the fourth, the trigger library was deleted.
Date and environment
- date → 2026-09-02 · four slices · verified live
- gone → smooth scrolling + scroll trigger
- kept → animation core · text splitting · line drawing
- method → animation-timeline: view() · IntersectionObserver
Result
The vendor bundle went from 51.1 to 33.5 kilobytes compressed; the trigger library alone was 17.6 kilobytes and shipped on every page. Twelve scenes out of twelve moved onto CSS timelines. Window scroll listeners went to zero — the remaining four are exemptions marked with their reason in the source. Live, the worst real-user LCP fell from 592–640 milliseconds to 440, blocking time to 0–10 milliseconds, and lab layout shift stayed at zero.
- vendor bundle → 51.1 → 33.5 KB (gzip)
- CSS scenes → 12 / 12
- scroll listeners → 0 (4 reasoned exemptions)
- real LCP (worst) → 592–640 → 440 ms
The platform’s small print
The move was not free; measurement taught us in three places. A view timeline’s default inset counts the scrollport’s scroll padding, so the reading line started at 4.5% into the article; zeroed, it matched the old JavaScript formula at seven sample points exactly. For the range “top edge at the top, bottom edge at the top” the obvious keyword was not the right one but its crossing variant: on a section taller than the viewport the first choice shifted the range by 304 pixels and took its length from the viewport instead of the subject. And a counter that reads sibling order re-resolves inside a child element unless the custom property is registered — all three lines then got the same value.
A defect that left with the library
The library took a bug with it on the way out. In-page anchors sometimes did not land on their target; the root cause was that the library took the current scroll position as its target while being set up, and the browser applied the anchor scroll afterwards — its own loop then pulled the page back. The defect had been measured at the gate: with the library installed, one attempt in twenty was broken; in the two modes where it was never installed, zero. With native scrolling the browser goes to the anchor itself and the race is gone.
The lesson: your measuring tool is a dependent too
While the verification suite was fully green, the accessibility audit stopped with “ScrollTrigger is not defined”. The tool was reading a scene’s range from the retired library, and the suite cannot see that, because the audit runs outside the suite. When a library retires, you scan more than the product: you scan the instruments that measure it.
Limits
The animation core is still there: this site’s signature focus gesture and its measured layout locks still live in JavaScript. The plan’s “under thirty kilobytes” target therefore did not hold; it stopped at 33.5, and the budget ceiling locks the regression, not the target. In an engine without timeline support the scenes rest at their final values — what is lost is motion, not content.
Scrolling is not an event, it is a timeline.