Skip to content
Craft & CSS

scroll-timeline, honestly

Scroll-driven animations in pure CSS — progress bars that fill with the scroll, sections that animate as they pass. What scroll-timeline can do today, where it still breaks, and the fallback that keeps your content honest.

Answers Should I use scroll-linked animation?sometimes yes, often no, and the path shows the version that stays on the compositor.

Advanced2 min read (computed · recorded 10)updated 2026-09-10scrollscroll-timelineanimationcss

The two timelines

Scroll-driven animation comes in two flavours. The scroll progress timeline: the animation is bound to how far the scroller has travelled (a reading progress bar, a hero that compresses as you leave). The view progress timeline: the animation is bound to how far an element has travelled through the viewport (a section title that reveals as it enters). Both are declarative — no rAF loop, no scroll listener, no layout-thrash opportunity — which is the real gift.

scroll-timeline.css
/* reading progress: bound to the scroller */
@keyframes fill { from { scale: 0 1; } to { scale: 1 1; } }
.progress {
  transform-origin: left;
  animation: fill linear both;
  animation-timeline: scroll(root);
}

/* section title: reveals as it crosses the viewport */
@keyframes rise { from { opacity: 0; translate: 0 24px; } }
.section h2 {
  animation: rise linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
}

What it cannot do — today

  • No scroll-linked easing curves: the animation follows the scroll linearly; you cannot make a section feel 'spring-loaded' against the scroll position without JavaScript sampling scroll velocity.
  • No scrubbing other properties cheaply: animating anything except transform and opacity still runs layout/paint per scroll frame — the same GPU rule as every other animation applies.
  • Timeline units and ranges are new and verbose; animation-range syntax (entry, exit, contain) still trips people, and browser prefixes have not fully settled.
  • Fallbacks matter: in browsers without support the animation never runs — content stays visible only if you write the 'from' state as the resting state, which is exactly the honest pattern: no support means no animation, never no content.
The support pattern

Write the resting state first (content fully visible, progress at zero). Then layer the scroll-driven animation on top with @supports (animation-timeline: scroll()). No support = clean static page. Support = the enhancement. Scroll-driven motion must never be the only way content appears.

Where it genuinely beats JavaScript

A reading progress bar, a scroll-vignette that deepens as you leave the hero, an image that settles into place as it enters — these are one-liners in scroll-timeline and hundreds of lines of fragile listener code by hand. The bar is also perfectly synced by the browser: no rAF drift, no scroll-position rounding, no jank from a listener fighting the compositor. For atmosphere that tracks the scroll, the platform finally has the primitive.

Practise the lesson

Theory sticks when you ship it. These original Motif assets put this guide's lesson to work — open one and copy it into your own page.