EduFlow
Modern CSS Mastery
0%

Section 4 · Motion and Architecture

Scroll-Driven Animations

17 min3 min readCode playground

Lesson at a glance

Drive CSS animations from scroll position with scroll() and view() timelines, choose animation ranges, and ship them as a progressive enhancement while Firefox support is pending.

  1. 1Two kinds of timeline
  2. 2Reveal on view, with ranges
  3. 3Ship it as an enhancement
  4. 4Your turn

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Waypoint’s schedule is long: two days, four tracks, sixty sessions. The team wants two small touches: a thin progress bar under the header showing how far down the schedule you are, and speaker cards that ease into view as you scroll to them. The old way was a scroll event listener that read scrollY, did some arithmetic and wrote styles, sixty times a second, on the main thread, competing with everything else.

Scroll-driven animations let CSS do it. You write a normal @keyframes animation and swap its clock: instead of time, its progress comes from scrolling.

Two kinds of timeline

A scroll progress timeline, scroll(), goes from 0% to 100% as a scroll container scrolls from top to bottom. scroll(root) is the document; scroll(nearest), the default, is the nearest scrolling ancestor.

A view progress timeline, view(), follows one element as it travels through its scroll container’s visible area (the scrollport): from the moment its edge peeks in at the bottom to the moment it leaves at the top.

progress.css
@keyframes grow {
  from { scale: 0 1; }
  to   { scale: 1 1; }
}

.progress {
  position: fixed;
  inset: 0 0 auto;
  height: 3px;
  background: var(--track-design);
  transform-origin: 0 50%;
  animation: grow linear both;
  animation-timeline: scroll(root);
}

At the top of the page the bar is scaled to zero; at the bottom it’s full width; anywhere in between it’s exactly proportional. linear matters here: with the default ease, the bar would race ahead in the middle and crawl at the ends. both keeps the first and last keyframes applied outside the range.

Reveal on view, with ranges

For the speaker cards, a view timeline is the right clock, and animation-range chooses which part of the card’s path runs the animation:

speakers.css
@keyframes reveal {
  from { opacity: 0; translate: 0 24px; }
  to   { opacity: 1; translate: 0 0; }
}

.speaker {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}
A tall scrollport rectangle. A card is shown at three positions: just entering at the bottom edge, fully inside, and leaving at the top edge. Labels mark the entry range at the bottom edge, the contain range while fully inside, and the exit range at the top edge; cover spans from first entry to final exit. scrollport card card card entry contain exit cover scrolling down
A view timeline follows an element through the scrollport; named ranges mark parts of that path

The named ranges describe the element’s path: entry while it crosses the bottom edge, contain while it’s fully inside (or fully covering, if it’s taller), exit while it crosses the top edge, and cover for the whole trip from first pixel in to last pixel out. entry 0% cover 30% means “start as the card begins to enter, finish when it’s 30% through its whole trip”, so each card is fully visible well before the user reaches it.

Ship it as an enhancement

Here’s the support picture in October 2026: scroll-driven animations have been in Chromium since version 115 (2023) and in Safari since 26 (September 2025). Firefox has an implementation behind a flag, and it’s an Interop 2026 focus area, but stable Firefox doesn’t support it yet, so the feature isn’t Baseline.

That shapes how you write it. The reveal keyframes start at opacity: 0, and in a browser that understands animation-timeline, that’s fine. In one that doesn’t, animation: reveal linear both with no timeline is a zero-duration time animation that ends at its last keyframe, so the cards still show. Don’t rely on that subtlety; make the intent explicit:

CSS
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .speaker {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }
  }
}

Unsupported browsers and reduced-motion users get the plain, fully visible grid. Supported browsers get the reveal.

You can also name timelines (view-timeline-name: --speaker) so another element can follow them, and use timeline-scope to make a name visible higher in the tree. Reach for those when one element’s scroll position should drive a different element; for self-contained effects, anonymous scroll() and view() are enough.

Your turn

In the exercise, add the reading progress bar and the speaker reveal, in the right order, guarded by @supports. The checks read the computed timelines and ranges. Next, you’ll step back from individual effects and organise the whole stylesheet so a team can work on it.

Key takeaways

  • animation-timeline: scroll() ties an animation’s progress to how far a scroll container has scrolled; view() ties it to an element’s passage through the visible area.
  • animation-range picks which part of the timeline plays the animation, for example entry 0% cover 30% for a reveal that finishes soon after the element appears.
  • The animation shorthand resets animation-timeline, so declare the timeline after the shorthand.
  • Scroll-driven animations work in Chromium and Safari 26 but not yet in stable Firefox, so wrap them in @supports and make sure content is fully visible without them.

Further reading

Check your understanding

3 questions

Question 1 of 3

The progress bar never moves. Why?

CSS
.progress {
  animation-timeline: scroll(root);
  animation: grow linear both;
}
Question 2 of 3

A speaker card should fade in as it enters the viewport and be fully visible by the time 30% of it is showing. Which declarations?

Question 3 of 3

In 2026, a teammate wants to make the speaker grid hidden until scroll-driven reveals fade each card in. What’s the main risk?

Practice

Code playground

Scroll-driven progress and reveals

The grow and reveal keyframes are ready. Wire them to scrolling:

  1. Fix the progress bar so grow runs on the document’s scroll timeline, with linear timing and a fill mode that holds the keyframes (both).
  2. Make each .speaker run reveal on its view timeline, from the start of its entry to 30% of its cover range.
  3. Put the scroll-driven rules inside @supports (animation-timeline: view()) so browsers without support show the plain page. (Wrapping them in prefers-reduced-motion: no-preference too is recommended.)
<div class="progress" aria-hidden="true"></div>
<h2>Speakers</h2>
<section class="speakers">
  <article class="speaker">Ana Ortiz</article>
  <article class="speaker">Kenji Watanabe</article>
  <article class="speaker">Layla Haddad</article>
  <article class="speaker">Tom Becker</article>
  <article class="speaker">Priya Nair</article>
  <article class="speaker">Marcus Lee</article>
</section>Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • The bar’s animation runs on a scroll() timeline
  • The bar uses linear timing and keeps its keyframes
  • Every speaker card runs reveal on a view() timeline
  • The reveal runs from entry to cover 30%
  • The rules are inside @supports (animation-timeline: …)