TL;DR: CSS Scroll-Driven Animations let you animate elements based on scroll position – entirely without JavaScript. Withanimation-timeline: scroll()andanimation-timeline: view(), you can build progress bars, fade-ins, and parallax effects in pure CSS. Performant, elegant, and future-proof.
🤔 What Are Scroll-Driven Animations?
Imagine scrolling through a website and elements move, fade in, or transform – all perfectly synced to your scroll position. Sounds like a lot of JavaScript? Not anymore.
CSS Scroll-Driven Animations are a new CSS specification that lets you tie existing CSS animations to scroll progress. Instead of a time-based animation, the animation runs as fast or slow as you scroll. No requestAnimationFrame, no IntersectionObserver, no event listeners. Pure CSS.
There are two main concepts:
- Scroll Progress Timeline – The animation is tied to the overall scroll progress of a container.
- View Progress Timeline – The animation starts and ends when an element scrolls into the viewport.
🕰️ The Old Way: JavaScript and IntersectionObserver
Until now, the standard approach for scroll-based animations was a mix of JavaScript and CSS. You'd either use a scroll event listener or the IntersectionObserver to toggle classes.
A typical example looked something like this:
// The old way with IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.animate-on-scroll').forEach(el => {
observer.observe(el);
});
/* Plus the CSS class */
.animate-on-scroll {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s, transform 0.6s;
}
.animate-on-scroll.visible {
opacity: 1;
transform: translateY(0);
}
This works. But it has downsides: you need JavaScript, the animation isn't truly tied to scroll position (it only triggers once), and with many elements, performance can suffer.
🚀 The New Way: Pure CSS with animation-timeline
Here's where it gets exciting. With CSS Scroll-Driven Animations, you only need CSS. The key is two new properties:
animation-timeline: scroll()– Ties the animation to scroll progress.animation-timeline: view()– Ties the animation to an element's visibility in the viewport.
scroll() – The Scroll Progress Timeline
With scroll(), you tell the browser: "Hey, use the scroll progress as the timeline for this animation." The function accepts two optional parameters:
animation-timeline: scroll(<scroller> <axis>);
/* Examples */
animation-timeline: scroll(); /* nearest scrollable ancestor, block axis */
animation-timeline: scroll(root); /* the root element (viewport) */
animation-timeline: scroll(nearest); /* nearest scrollable ancestor */
animation-timeline: scroll(root inline); /* root element, inline axis */
view() – The View Progress Timeline
view() is perfect for elements that should animate when they scroll into the visible area. Think fade-ins, slide-ins, or scale effects.
animation-timeline: view();
animation-timeline: view(block); /* block axis (default) */
animation-timeline: view(inline); /* inline axis */
animation-timeline: view(block 50px); /* with inset */
🎯 Practical Example 1: Scroll Progress Bar
A classic example: a progress bar at the top of the page showing how far you've scrolled. This used to require JavaScript. Now? Three CSS rules.
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(to right, #6366f1, #8b5cf6);
transform-origin: left;
/* The magic */
animation: grow-progress linear;
animation-timeline: scroll(root);
}
@keyframes grow-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
That's it. No JavaScript. The bar scales automatically based on your scroll position. Smooth, performant, and declarative.
🎯 Practical Example 2: Fade-In on Scroll
Want elements to fade in as they scroll into view? With view(), it's a breeze:
.fade-in {
animation: fade-in-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
The element starts invisible and fades in as it enters the viewport. animation-range: entry 0% entry 100% means: the animation runs during the entire "entry" phase – from the first pixel until the element is fully visible.
🎯 Practical Example 3: Parallax Effect
Parallax effects used to be a JavaScript nightmare. Now you can do it in pure CSS:
.parallax-element {
animation: parallax linear both;
animation-timeline: view();
}
@keyframes parallax {
from {
transform: translateY(-50px);
}
to {
transform: translateY(50px);
}
}
The element moves slower than the rest of the page. Subtle, elegant, and without a single line of JavaScript.
🎚️ animation-range: Precise Control
The real power lies in animation-range. It lets you control exactly when the animation should start and end. There are several named ranges:
| Range | Description |
|---|---|
entry | Element enters the viewport |
exit | Element exits the viewport |
contain | Element is fully visible |
cover | From first to last visible pixel |
/* Only animate during entry */
animation-range: entry;
/* Only the first 50% of entry */
animation-range: entry 0% entry 50%;
/* From entry to exit */
animation-range: entry 0% exit 100%;
/* Only when fully visible */
animation-range: contain;
/* With fixed values */
animation-range: entry 20px entry 200px;
This gives you incredibly fine-grained control over your animations. For example, an element that only fades in during the first half of entry and then stays put.
⚙️ @keyframes in a Scroll Context
Important point: your @keyframes work exactly the same as always. The only difference is that the timeline isn't measured in seconds but in scroll progress (0% to 100%).
@keyframes reveal {
0% {
opacity: 0;
transform: scale(0.8) rotate(-5deg);
filter: blur(10px);
}
50% {
opacity: 1;
filter: blur(0);
}
100% {
transform: scale(1) rotate(0deg);
}
}
.reveal-element {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry;
}
The beauty of it: you can define complex, multi-step animations that perfectly adapt to scroll position. Everything stays declarative and predictable.
🌐 Browser Support and Progressive Enhancement
As of March 2026, browser support is looking pretty solid. Chrome and Edge have supported Scroll-Driven Animations since version 115. Firefox has had them enabled by default since version 131. Safari supports them in newer versions as well.
Still, you should use progressive enhancement. The @supports rule is your best friend:
/* Base styling without animation */
.element {
opacity: 1;
transform: none;
}
/* Scroll animation only when supported */
@supports (animation-timeline: view()) {
.element {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry;
}
}
This way your site works everywhere – with animations where possible, without as a fallback. No user sees a broken page.
⚡ Performance: CSS vs. JavaScript
Why is the CSS approach better? Simple:
- Main thread stays free: CSS animations run on the compositor thread. JavaScript-based animations block the main thread.
- No layout thrashing: The browser optimizes CSS animations automatically. With JS, you have to manage that yourself.
- Less code: No JavaScript bundle that needs to be loaded and parsed.
- Automatic optimization: The browser can automatically throttle CSS animations when they're not visible.
In my testing, the difference on pages with many animated elements was clearly noticeable. Less jank, smoother animations, better Core Web Vitals.
💡 Conclusion
CSS Scroll-Driven Animations are a game changer. What used to require dozens of lines of JavaScript now works with a handful of CSS properties. The API is intuitive, performance is excellent, and browser support has become solid.
My tip: start with the scroll progress bar. It's the simplest example and you'll immediately see how powerful this is. Then experiment with view() and animation-range for fade-ins and reveal effects.
The future of web animations is declarative. And it's here now.
Discover more articles
CSS Scroll-Driven Animations – Animationen ohne JavaScript 🎬
TL;DR: CSS Scroll-Driven Animations ermöglichen es dir, Elemente basierend auf der Scroll-Position zu animieren – ganz ohne JavaScript. Mit animation-timeline: scroll() und animation-timeline: view() kannst du Fortschrittsbalken, Fade-Ins und Parallax-Effekte rein deklarativ umsetzen. Performant, elegant und zukunftssicher. 🤔 Was sind Scroll-Driven Animations? Stell dir vor, du scrollst durch eine Website und
CSS Grid: The Missing Counterpart to Flexbox 🧩
TL;DR: CSS Grid is the missing puzzle piece to your Flexbox knowledge. While Flexbox handles one-dimensional layouts, Grid gives you full control over rows AND columns simultaneously. This article covers everything from basics to subgrid – with practical examples you can use right away. You know Flexbox? Great. Time to
View Transitions API: Smooth Page Transitions Without a Framework ✨
TL;DR: The View Transitions API brings buttery-smooth page transitions directly to the browser - no framework, no JavaScript library needed. Just CSS and a few lines of JS. Here's how it works and why you should start using it today. 🤔 What Are View Transitions Anyway? You know
CSS :has() – The Parent Selector Everyone’s Been Waiting For 🎯
TL;DR: CSS :has() is the long-awaited parent selector – you can finally style parent elements based on their children. No more JavaScript hacks, no workarounds, just pure CSS. And yes, it works in all modern browsers. 🤔 Why CSS Never Had a Parent Selector If you've been writing CSS
Container Queries in CSS: The End of Media Queries? 📦
Container Queries put an end to viewport chaos! 📦 How to make your components truly responsive – and why Media Queries are not dead yet.