Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 12.1Intermediate ~20 min · shot 1 of 7

Scroll Progress Timelines

In one line

A scroll progress timeline ties an animation to how far a scroller has scrolled: 0% at the top, 100% at the end. animation-timeline: scroll() does it in one line, with no scroll listener, and the animation has no duration in seconds.

0/7 · 0%

Think of it like this

A book's thumb-index. When you riffle the pages, the position of the pages decides what you see, not a clock. Stop riffling and the picture stops. Flip back and it goes backwards. A scroll-driven animation: the scroll bar is the clock.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

timeline
The thing that says what time it is for an animation. The document timeline counts milliseconds; a scroll timeline counts scroll progress.
scroll progress timeline
A timeline made from a scroller's offset: 0% at the start, 100% at the end. Written scroll().
block axis
The direction in which blocks of text stack. In English it is vertical. inline is the direction text runs: horizontal.
inactive timeline
A scroll timeline with no range because there is nothing to scroll. Its progress is null and the animation has no effect.

Step by step

01Press Middle and read the track

In "A reading bar from the scroll position" press Middle: the bar fills to half and the track is a Scroll track at 50%. End gives 100%.

02Pause the clock and scroll again

Press Pause, then Middle. The bar still moves: the scroll bar is its clock.

03Read the console

It says CSSAnimation on a ScrollTimeline, source the scroller, axis block, duration auto. scroll() chose all of it.

04Open the five cases

In "Five scrollers, five outcomes" the short box says none, the 10 s bar behaves like the others, and the bad-order bar is full at load.

Try it yourself

  1. 1

    Make the bar start late and finish early

    In "A reading bar…" add animation-range: 25% 75%; after the timeline line. Measured by scrolling to 0, 25%, 50%, 75% and 100% of the way: the bar is at 0, 0, 0.50, 1 and 1. The bar waits through the first quarter and is full for the last quarter. animation-range picks which part of the scroll the keyframes spread over (Topic 12.2 explains it).

Code & diagrams

A reading bar from the scroll position

The bar is sticky at the top of a 140 px scroller. Press Top, Middle and End, or scroll with the mouse, and read the Scroll track.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

supports scroll(): true
animations: 1 CSSAnimation on ScrollTimeline
source is the scroller: true · axis: block
animation-duration: auto
Five scrollers, five outcomes

Each box has a bar. The numbers are the timeline's progress. The last box wrote `animation-timeline` before the `animation` shorthand.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

at load: tall 0%, short none, sideways 0%, 10 s 0%
scrolled to the end: tall 100%, short none, sideways 100%, 10 s 100%
bad order: DocumentTimeline, bar already full: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Put animation-timeline before the shorthand

In "A reading bar…" swap the two lines so the CSS reads animation-timeline: scroll(); animation: fill linear both;.

terminal
$ Reload and scroll
── what you'll see ──
The bar is full at once and never moves. The track is not a Scroll track: the animation runs on the document timeline. In "Five scrollers…" the last line reads `bad order: DocumentTimeline, bar already full: true`.
From a real run of the fifth box.

Myth vs fact

Myth

Scroll-driven animations are the same as listening for the scroll event.

Fact

A listener runs your JavaScript on the main thread. A scroll timeline is declared in CSS and the browser applies it. In Chrome it can run off the main thread for transform and opacity.

Remember this

  1. 1

    The problem this solves: to show reading progress, people used to listen for scroll events and set a width on each one. That runs on the main thread and stutters when the page is busy. animation-timeline lets the browser tie an animation to the scroll position itself. (The CSS course, Topic 10.6, shows this in a few lines; here are the rules underneath.)

  2. 2

    A timeline says what time it is. Every animation so far used the document timeline: milliseconds since the page started. animation-timeline: scroll() swaps it for a scroll progress timeline: its "time" is the scroll offset as a percentage, 0% at the scroller's start and 100% at its end. The keyframes are spread over that range.

  3. 3

    **scroll() takes two choices:** scroll(<scroller> <axis>). The scroller is nearest (the closest ancestor that scrolls, the default), root (the page) or self. The axis is block (the direction text stacks, usually vertical, the default), inline, x or y. The first example writes scroll() and gets the nearest scroller and the block axis: its console proves both.

  4. 4

    There is no duration in seconds. The duration is auto: the whole 0 to 100%. If you write animation: fill 10s linear both, the 10s is ignored (see the "10 s" box in the second example). Easing still applies to the scroll progress, so ease-in makes a bar lag: use linear.

  5. 5

    Order matters. The animation shorthand resets animation-timeline to auto (the document timeline). Write animation-timeline after the shorthand. Get it wrong and nothing is reported: the animation runs on the clock with duration 0, so the bar is full at once.

  6. 6

    Edge cases. A scroller with nothing to scroll has no range: its timeline is inactive, its progress is null, the animation does not apply, and the element shows its own style (an empty bar). A sideways scroller needs the inline axis. And an element needs a scroller above it: the bar is position: sticky inside the box, so its nearest scroller is the box.

  7. 7

    Support and a safe fallback. animation-timeline works in about 88% of browsers in use (caniuse, 2026-10-24: 87.84%; Chrome 115, Safari 26, Firefox 159). Write the effect inside @supports (animation-timeline: scroll()): the page must be complete without it. In Chrome a scroll-driven transform or opacity animation can run off the main thread (Chrome's documentation says so; this course cannot measure it). On the Motion Timeline these are Scroll tracks, and Pause does not stop them: only scrolling moves them.

Explain it without notes

01

How is a scroll progress timeline different from the document timeline?

02

Why is animation-duration useless with animation-timeline: scroll(), and what do you use instead?

Practice

01

Make a bar that follows the sideways scroll of a gallery row (overflow-x: auto), not the page's vertical scroll.

02

Write the @supports and reduced-motion rules for a progress bar that fills with the page's scroll, so that without support or with reduced motion it is not shown at all.

Done when you can

  • You can write animation-timeline: scroll() and say which scroller and axis it picks

  • You know why the animation shorthand must come before animation-timeline

  • You can explain what happens with nothing to scroll

  • You wrap the effect in @supports and a reduced-motion rule