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.
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.
@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:
@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%;
}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:
@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-rangepicks which part of the timeline plays the animation, for exampleentry 0% cover 30%for a reveal that finishes soon after the element appears.- The
animationshorthand resetsanimation-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
@supportsand make sure content is fully visible without them.