CSS scroll timelines or ScrollTrigger.
Both turn scrolling into animation; the difference is not speed but where the work runs — the browser’s own timeline, or a library on the main thread.
The question looks like “which is faster”, but the decision is made elsewhere: who computes the scene’s progress, and where does that computation run? On a scroll timeline the browser computes it; with a library JavaScript computes it and writes to the DOM on every frame. Most of the other differences follow from that single one.
The platform side: an animation bound to a timeline
In CSS an animation can be bound to scrolling instead of time: `animation-timeline` picks the timeline (the scroll container itself, or an element’s visibility) and `animation-range` says which part of that timeline is used. Because the range is written in keywords, the measurement happens in the browser rather than in your code, and a change of viewport size needs no recalculation of your own.
It has a cost: the range vocabulary is small but sharp. For a range like “top edge at the top, bottom edge at the top” you need the crossing variant rather than the obvious keyword; on a section taller than the viewport the wrong choice shifts the range by hundreds of pixels. And the default inset counts the scrollport’s scroll padding, so the space left for anchor links can move where the timeline starts.
The library side: every value is in your hands
A library is a layer that turns scrolling into a timeline and hands you a callback on every frame. The cost is measured in bytes and main-thread work; the gain is control. If you want to read the progress and draw it to a canvas, update a counter’s text or announce an accessibility state, you want the value in JavaScript anyway. For scenes that hold layout, the library’s wrapper is a ready-made way of making room without breaking the page’s rhythm — but because the wrapper changes the tree, it can also change what your selectors match.
They do not replace each other; they split the work
In practice the line sits here: if what CSS can do alone is a visual transform (translate, scale, opacity, blur, colour) and reversing it on scroll-back is correct, the timeline is enough. If the value has to be written to text, to a canvas or to the accessibility tree, JavaScript is unavoidable — and even then the timeline can stay in CSS, with JavaScript reading progress from the animation itself. That is exactly the bridge used on this site: the scene’s progress comes from the CSS animation, the writing side is JavaScript.
Mobile and motion preference
Both approaches must respect a reduced-motion preference, but the gate goes in different places: in CSS the rule is wrapped in a media query, with a library the setup simply never happens. Another difference is two-way behaviour — a scroll timeline is bidirectional and rewinds when you scroll back. Gestures that should happen once (typing, opening, counting) are therefore bound to a one-shot trigger rather than to the timeline.
The short answer: the platform side carries most scenes today at zero bytes; the library remains the shortest path where it is genuinely shorter — in conditional flows, where old-engine behaviour must be guaranteed, and wherever progress has to become a value. On this site the decision was made by measurement, and both libraries retired: scrolling is now a CSS timeline.
Criterion
- Where it runs — CSS scroll timeline: In the browser’s animation engine; in supporting engines it can move to the compositor · ScrollTrigger: On the main thread, as JavaScript
- Bytes shipped — CSS scroll timeline: None — the syntax is part of the platform · ScrollTrigger: A library file ships (17.6 kilobytes on this site, on every page)
- Browser support — CSS scroll timeline: Runs where supported; where it is not, the animation is not set up and the element rests at its final value · ScrollTrigger: The library sets up the same behaviour in every engine
- Range vocabulary — CSS scroll timeline: `animation-range` keywords: cover · contain · entry · exit and their crossing variants · ScrollTrigger: start/end strings and numeric offsets
- Pinning a scene — CSS scroll timeline: The element translates on its own timeline; no wrapper needed · ScrollTrigger: A wrapper (pin-spacer) is inserted and inline measurements are written to the element
- Scenes that write text, ARIA or canvas — CSS scroll timeline: CSS cannot do it alone; progress is bridged to JavaScript · ScrollTrigger: Native: a callback hands you the value on every frame
- Debugging — CSS scroll timeline: The browser’s animation panel; intermediate values via `getAnimations()` · ScrollTrigger: The library’s markers and its own API
- Dependency and versions — CSS scroll timeline: None — no package to upgrade · ScrollTrigger: Version upgrades, licence and compatibility to track
- Where it fits — CSS scroll timeline: Visual, reversible, scroll-bound scenes · ScrollTrigger: Conditional flows, complex timelines, guaranteed behaviour in older engines
FREQUENTLY ASKED QUESTIONS
Can a CSS timeline carry every scene?
No. Where a scene writes text, draws to a canvas or updates ARIA, CSS carries the progress but JavaScript still writes the value. On this site twelve scenes out of twelve moved to CSS timelines; the focus gesture and the canvas engines stayed in JavaScript.
What happens in a browser without support?
The animation is never set up and the element rests at its final value in the markup. What is lost is motion, not content. This is a behaviour you can design for: leave the scene readable at its last frame.
What does removing the library measure out to?
Two things: the bytes shipped and a sync loop running on every frame. On this site the vendor bundle went from 51.1 to 33.5 kilobytes, window scroll listeners went to zero, and the worst real-user LCP fell from 592–640 milliseconds to 440.