The Problem with JavaScript Scroll Events
Scroll-based effects have traditionally relied on JavaScript scroll event listeners running on the main thread, which often causes jank, dropped frames, and poor user experience. Even with passive listeners and requestAnimationFrame throttling, layout thrashing remains a persistent issue. CSS Scroll-Driven Animations solve this by moving scroll tracking to the compositor thread, enabling smooth 60fps effects without any JavaScript overhead.
Key Concepts
Three core concepts power this feature. A scroll-timeline links an animation to the scroll position of a scroll container. A view-timeline ties an animation to an element’s visibility within a scroll container. The animation-timeline property binds a named animation to either timeline type. Use timeline-scope to share timelines across elements in the DOM tree.
Scroll Progress Timelines
A scroll progress timeline tracks how far a user has scrolled through a container. The modern approach uses the scroll-timeline shorthand and the inline scroll() function:
@keyframes fill-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
animation: fill-progress linear;
animation-timeline: scroll();
}
The scroll() function creates an anonymous timeline on the nearest scroll container. For named timelines, use scroll-timeline-name and scroll-timeline-axis on the scroller, then reference it with animation-timeline: --my-timeline.
View Progress Timelines
View timelines trigger animations based on element visibility within the viewport. Set view-timeline-name on the observed element and bind the animation:
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.reveal {
animation: fade-in linear;
animation-timeline: view();
animation-range: entry 0% cover 50%;
}
The animation-range property controls when animation starts and ends. Range names include entry (element entering), exit (element leaving), contain (fully within), and cover (fully visible). Combine them for precise control over timing.
Practical Example: Reading Progress Indicator
A fixed progress bar that fills as the user scrolls is one of the most practical applications:
body {
scroll-timeline-name: --page-scroll;
}
.progress {
position: fixed; top: 0; left: 0;
height: 4px; z-index: 100;
background: linear-gradient(90deg, #667eea, #764ba2);
transform-origin: left center;
animation: fill-progress linear;
animation-timeline: --page-scroll;
}
Practical Example: CSS-Only Parallax
CSS parallax eliminates JavaScript entirely. Layer multiple elements with different animation-range values to create depth:
.layer-back { animation-range: entry 0% exit 100%; }
.layer-front { animation-range: entry 0% exit 50%; }
No main-thread layout recalculations, no scroll event listeners — just smooth, composited parallax at 60fps.
Browser Support and Progressive Enhancement
| Browser | Version | Status |
|---|---|---|
| Chrome | 115+ | Full support |
| Safari | 17.2+ | Behind flag |
| Firefox | 121+ | Partial |
Use @supports for progressive enhancement:
@supports (animation-timeline: scroll()) {
.progress { animation: fill-progress linear; animation-timeline: scroll(); }
}
Provide a static fallback first, then layer scroll-driven animations as an enhancement for supporting browsers.
Conclusion
CSS Scroll-Driven Animations represent a paradigm shift for scroll-based effects. By moving scroll tracking to the compositor thread, they guarantee smooth performance while dramatically simplifying code. As browser support matures, these features will become essential tools for creating engaging, performant web experiences.
