Lesson 19 of 30
Tying an animation's progress to scroll position used to mean a scroll listener recalculating on every tick on the main thread. animation-timeline lets the compositor do it instead.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
What is the core performance advantage of animation-timeline over a JS scroll event listener?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The traditional way to make something animate as the user scrolls — a progress bar filling, an element fading in as it enters view — is a scroll event listener in JavaScript that reads the scroll position on every tick and updates styles accordingly. That works, but scroll events fire a lot (potentially every frame during a scroll gesture, at 60-120fps depending on the display), and the listener runs on the main thread — the same thread handling layout, JS execution, and everything else. Even a cheap handler adds up, and a janky scroll handler is a classic source of visibly stuttery scrolling.
animation-timeline ties a normal CSS @keyframes animation's progress directly to a scroll position instead of to elapsed time, and the browser computes that progress on its own — off the main thread, in the compositor. You write a keyframe animation exactly like any other CSS animation; the only difference is what drives its progress: scroll offset instead of a clock.
There are two flavors:
scroll() ties an animation's progress to a scrollable container's scroll offset — 0% progress at the top (or start) of the scroll range, 100% at the bottom (or end). This is the shape for something like a reading-progress bar that fills as the whole page scrolls.view() ties an animation's progress to how far a specific element has scrolled through the viewport — 0% as it enters, 100% as it exits (with configurable insets for when exactly "entered" and "exited" count). This is the shape for a "fade and slide in as this card scrolls into view" effect./* scroll(): progress tied to the whole page's scroll offset */
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
transform-origin: left;
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
/* view(): progress tied to this element's position in the viewport */
@keyframes fade-in {
from { opacity: 0; translate: 0 20px; }
to { opacity: 1; translate: 0 0; }
}
.card {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
Neither of these needs a single line of JavaScript, and neither one runs a per-frame scroll handler on the main thread — the browser recalculates progress as part of compositing, which is why this stays smooth even when the main thread is busy with something else (a common cause of janky JS-driven scroll effects in the old approach).
scroll() and view() answer different questions. scroll() is "how far has the container scrolled," view() is "how far has this element traveled through the viewport" — reaching for the wrong one is a common source of an animation that plays at the wrong time.@keyframes animation underneath. animation-timeline doesn't replace keyframes, it replaces what drives their progress — animation-duration becomes irrelevant once a scroll timeline drives progress instead of time, which trips people up since it's easy to leave a stale duration value in place out of habit.@supports (animation-timeline: scroll()) if a hard guarantee is needed."How would you build a scroll-progress indicator without a scroll event listener, and why would that matter for performance?" tests whether a candidate understands the main-thread cost of scroll handlers and knows the CSS-native alternative now exists.