Scroll-Driven Animations
Scroll-driven animations link an animation's progress directly to scroll position — instead of time, the timeline is the scrollbar. Scroll a bit, the animation advances a bit; scroll back, it rewinds. For years this required a JavaScript scroll listener recalculating styles on every frame (a common performance bottleneck). Modern CSS does it natively with the animation-timeline property, driven entirely by the compositor thread, with no JavaScript and no per-frame scroll handler at all.
Two kinds of timeline
Scroll progress timeline | View progress timeline | |
|---|---|---|
Driven by | How far a scroll container has scrolled | How far an element has traveled through the viewport |
Function | scroll() | view() |
0% to 100% | Container scroll starts to scroll ends | Element enters the viewport to element exits the viewport |
Typical use | A reading progress bar tied to the whole page | An element fading or sliding in as it scrolls into view |
Scroll progress: a reading progress bar
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: #0066cc;
width: 100%;
transform-origin: left;
/* Link this element's animation to the page's scroll position */
animation-name: grow-progress;
animation-timeline: scroll(root);
/* "root" = the document itself is the scroll container */
}
@keyframes grow-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}There is no animation-duration here — the scroll position itself IS the timeline. At the top of the page the keyframe is at 0% (scaleX(0)); scrolled all the way to the bottom, it's at 100% (scaleX(1)). Scrolling up rewinds the bar instantly.
View progress: fade in as it enters the viewport
.card {
animation-name: fade-in-up;
animation-timeline: view();
/* Timeline covers this element's journey through the viewport */
animation-range: entry 0% cover 40%;
/* Animation plays only while the element is entering,
finishing once it's 40% into the viewport */
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}As each .card scrolls up into view, it fades in and slides up — purely from CSS, tied to that specific element's visibility, not a global scroll position. Scroll back up and it reverses just as smoothly.
Why this matters
Runs off the main thread — no jank from JavaScript scroll listeners fighting layout and paint.
Declarative — the relationship between scroll and animation state lives entirely in CSS.
Naturally reversible — scrolling back up just rewinds the timeline, no extra logic needed.