In CSS, what does animation-timeline: scroll() do to an animation, and how does that differ from the default document timeline?
answer
- progress, not elapsed time
- 0% to 100% of the scrollable range
- scrubbed by the user, not played
- scroll(root block), scroll(self inline)
- duration auto covers the whole range
basics
~20 sanimation-timeline: scroll() drives an animation by a scroll container's scroll position instead of by elapsed time. Progress 0% is scrolled fully to the start, 100% fully to the end, and scrolling back runs the animation backwards.
solid answer
~40 sBy default an animation is attached to the document timeline, which advances with the clock, so `animation-duration: 2s` means two seconds of wall time. `animation-timeline: scroll()` swaps that for a *scroll progress timeline*: the animation's progress is the scroll offset of a scroll container, from 0% at the start of its scrollable range to 100% at the end. There are no seconds involved — you leave `animation-duration` at `auto` so the keyframes stretch across the whole range, and narrow it with `animation-range` if you want less. The effect is scrubbing rather than playing: scroll up and the animation runs backwards, stop and it holds. `scroll()` takes an optional scroller and axis, as in `scroll(root block)` for page scroll or `scroll(self inline)` for the element's own horizontal scroll.
code
css · 15 lines@keyframes grow-bar {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
#progress {
position: fixed;
inset-block-start: 0;
inline-size: 100%;
block-size: 4px;
background: crimson;
transform-origin: left center;
animation: grow-bar linear;
animation-timeline: scroll(root block);
}go deeper
Be able to say that animation-timeline lets scroll position, rather than the clock, drive an animation, and recognise scroll() when you see it in a stylesheet.
Explain the mechanics: progress from 0% to 100% of the scrollable range, duration left at auto, the scroller and axis arguments of scroll(), and why scrolling back reverses the animation.
Show you can debug one. Name the silent failure modes — an inactive timeline because nothing overflows, nearest resolving to the wrong scroller, the animation shorthand resetting animation-timeline — and say how you would confirm each.
Own the tradeoff of declaring scroll linkage in CSS versus scripting it: the browser can drive compositable properties without main-thread work, but the effect is unavailable in browsers without support, so the base styles must stand on their own.
## Two kinds of timeline Every CSS animation is attached to a *timeline*, and the timeline is what supplies the animation's progress. The default is `animation-timeline: auto`, meaning the **document timeline** — a clock that starts when the page loads and advances in real time. That is why `animation-duration` is expressed in seconds: the animation asks the clock how much time has passed and maps it onto the keyframes. A **scroll progress timeline** replaces that clock with a scroll position. It has no seconds at all. It has a *scrollable range* — everything between fully scrolled to the start and fully scrolled to the end — and it reports where inside that range the container currently sits, as a progress value from 0% to 100%. ## What scroll() names `scroll()` creates an *anonymous* scroll progress timeline, meaning it is defined inline on the animated element rather than given a name. Its syntax is `scroll(<scroller> <axis>)`, both parts optional: - **scroller** — `nearest` (the default: the nearest ancestor that is actually a scroll container), `root` (the viewport's scrolling element), or `self` (this element, if it is itself a scroll container). - **axis** — `block` (the default), `inline`, `x`, or `y`. So `scroll(root block)` is "how far down the page are we", and `scroll(self inline)` is "how far along its own horizontal scroll is this carousel". ```css @keyframes grow-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } } #progress { transform-origin: left center; animation: grow-bar linear; animation-timeline: scroll(root block); } ``` ## Duration, easing, and range Leave `animation-duration` unset. Its `auto` behaviour on a progress timeline is "cover the whole timeline", which is almost always what you want; a time value there does not mean seconds of scrolling and is a classic source of an animation that appears finished before you have scrolled anywhere. To animate over only part of the scroll, use `animation-range` rather than a duration. The other animation longhands still apply, they just operate over progress instead of time. `animation-timing-function` eases the mapping from scroll progress to keyframe progress — which is why `linear` is the usual choice here, since any other curve makes the element appear to lag or race the user's finger. `animation-fill-mode` still controls what happens outside the active range, and `animation-iteration-count` above 1 makes the keyframes repeat within the scroll range. ## Scrubbing, not playing The biggest conceptual shift is that the animation is not *played*, it is *scrubbed*. Progress is a pure function of scroll position: scroll down and it advances, scroll up and it rewinds, stop and it freezes mid-way. Nothing queues, nothing catches up, and there is no notion of the animation being "late". This is also why the feature is a signal question. The JavaScript equivalent meant attaching a scroll listener, reading a scroll offset, and writing styles — work that lands on the main thread on every frame and stutters under load. Declared in CSS, the browser owns the mapping and can run it off the main thread when the animated properties are ones it can composite, such as `transform` and `opacity`. ## Why it silently does nothing Scroll-driven animations fail quietly — no console error — so know the usual causes: - **The scroller has no scrollable overflow.** A timeline whose range is zero is inactive, and an animation on an inactive timeline never progresses. This is why a `nearest` timeline often works in a tall page and dies in a short one. - **`nearest` picked a scroller you did not mean.** Any ancestor with scrollable overflow claims it, not just the one you were thinking of. - **The `animation` shorthand reset it.** `animation` resets `animation-timeline` to its initial value, so a shorthand written *after* `animation-timeline` wipes it. Always put the shorthand first, or use only longhands. ## Support Chromium shipped scroll-driven animations in Chrome 115 (2023); Safari and Firefox added support afterwards, so verify the current baseline for your audience rather than assuming universal support. Because the whole feature degrades to "the animation simply does not run", it wraps cleanly in `@supports (animation-timeline: scroll())` as an enhancement over a static base style.
- Why does putting the animation shorthand after animation-timeline break a scroll-driven animation?Because the `animation` shorthand resets `animation-timeline` to its initial value, `auto`. Writing `animation-timeline: scroll();` and then `animation: reveal linear;` in the same rule leaves the element back on the document timeline, so the animation runs once on load instead of following the scroll. Declare the shorthand first, or use only the longhands.
- What happens if the scroll container you targeted has nothing to scroll?The timeline is inactive — its scrollable range is zero — and an animation attached to an inactive timeline never progresses. Nothing renders and nothing is logged, which is why a scroll-driven effect that works on a long page can look completely broken on a short one. Check that the intended scroller actually overflows on the axis you named.
- Why is linear the usual timing function for a scroll-driven animation?Because `animation-timing-function` here eases the mapping from scroll progress to keyframe progress, not from time. Any non-linear curve makes the element accelerate or stall relative to the user's own scrolling, which reads as lag. Reserve easing for clock-driven animations, or bake the pacing into keyframe stop positions instead.
It is the difference between pressing play on a video and dragging the scrubber yourself: the frames are the same, but the position is whatever your finger says, and dragging left plays it backwards.
saying these in an interview costs you the question
- Thinks animation-duration in seconds controls a scroll-driven animation
- Says the animation plays once when the scroll position is reached
- Believes scroll() needs a JavaScript scroll listener underneath
- Assumes any ancestor works as the nearest scroller without checking overflow
- Writes the animation shorthand after animation-timeline and wonders why it broke