For years, creating animations that react to a user's scroll position has been the domain of JavaScript. We've relied on libraries and complex requestAnimationFrame
loops, constantly listening to scroll events and calculating element positions. While powerful, this approach often comes with a performance cost and can lead to janky experiences, especially on less powerful devices.
What if I told you that this entire class of effects—from progress bars to parallax backgrounds—is now possible with just a few lines of CSS?
Welcome to the next frontier of web animation. Thanks to a new CSS specification, we can now decouple animations from time and link them directly to scroll progress. This is a game-changer, moving complex logic from the main thread (JS) to the compositor thread (CSS), resulting in buttery-smooth animations.
In this article, we'll dive into how CSS Scroll-Driven Animations work and build three classic effects without writing a single line of JavaScript.
How Does It Work? The Core Concepts
The magic lies in two new CSS properties that work together:
-
animation-timeline
: This is the star of the show. It tells an animation to use a scrollbar's progress as its "timeline" instead of a traditional, time-based duration (like3s
). -
animation-range
: This property fine-tunes when the animation should start and end within the timeline. For example, you can make an animation only play when an element is first entering the viewport.
There are two main types of timelines you can create:
- Scroll Progress Timeline: Tracks the scroll position of the entire scroller (e.g., the whole page). This is perfect for things like a reading progress indicator. We create this with the
scroll()
function. - View Progress Timeline: Tracks an element's visibility as it moves through the viewport (or any scroll container). This is what you'll use for "reveal-on-scroll" effects. We create this with the
view()
function.
Enough theory. Let's build something!
Example 1: The Page Scroll Progress Indicator
This is the "Hello, World!" of scroll-driven animations. We'll create a simple bar at the top of the page that grows from left to right as the user scrolls down.
The HTML:
All we need is a single div
.
<div class="progress-bar"></div>
<article>
<!-- A lot of content here to make the page scrollable -->
</article>
The CSS:
First, we define a standard @keyframes
animation. This animation will simply scale our progress bar along the X-axis.
/* The keyframes are just a simple from/to */
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* The progress bar styling */
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 8px;
background-color: dodgerblue;
transform-origin: left; /* Animate from the left */
/* The magic! */
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
Let's break down the magic:
-
animation: grow-progress linear;
: We apply our animation as usual. Notice we don't need a duration! We also uselinear
so the progress is smooth and directly tied to the scroll position. -
animation-timeline: scroll(root block);
: This is the key.-
scroll()
: Creates a Scroll Progress Timeline. -
root
: Tells the browser to watch the document's main scroller. -
block
: Specifies that we are tracking the vertical scroll axis.
-
And that's it! You now have a silky-smooth, performant scroll progress bar.
Example 2: Reveal-on-Scroll Animations
A very common effect is to have elements fade in and slide up as they enter the viewport. With a View Progress Timeline, this becomes trivial.
The HTML:
Let's add a few images we want to animate.
<section>
<h2>Scroll Down to Reveal Images</h2>
</section>
<section>
<img src="..." alt="..." class="reveal-me">
</section>
<section>
<img src="..." alt="..." class="reveal-me">
</section>
The CSS:
Again, we start with keyframes. Then we apply them with a new timeline and a specific range.
/* Keyframes to fade and slide in */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(100px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal-me {
/* The magic! */
animation: fade-in-up linear;
animation-timeline: view();
animation-range: entry 0% entry 40%;
}
Let's break this one down:
-
animation-timeline: view();
: This is different!view()
creates a timeline based on the element's visibility in the scrollport. The animation will progress as the.reveal-me
element itself scrolls into view. -
animation-range: entry 0% entry 40%;
: This is the fine-tuning. It tells the browser:- Start the animation when the element begins to enter the viewport (
entry 0%
). - Finish the animation when the element is 40% of the way through its entry phase (
entry 40%
). - This prevents the element from suddenly appearing fully animated if you scroll very fast. It smoothly animates over that defined range.
- Start the animation when the element begins to enter the viewport (
Now, every element with the reveal-me
class will automatically get this effect without any observers or custom JS.
Example 3: Pure CSS Parallax Effect
Ready for something more advanced? Let's create a parallax effect where a background image moves at a different speed than the foreground content.
The HTML:
We'll use a container and place some text inside it. The background will be handled by a CSS pseudo-element.
<div class="parallax-container">
<h1>Parallax Title</h1>
</div>
The CSS:
This technique attaches the animating element (the ::before
pseudo-element) to the view timeline of its container.
.parallax-container {
position: relative;
height: 70vh;
display: grid;
place-content: center;
overflow: hidden;
}
.parallax-container::before {
content: '';
position: absolute;
inset: -20px; /* Give it extra room to move */
background-image: url('...');
background-size: cover;
background-position: center;
/* The magic! */
animation: parallax-scroll linear;
animation-timeline: view(block);
}
@keyframes parallax-scroll {
to {
transform: translateY(var(--parallax-speed, 20%));
}
}
Let's break it down:
- We create a
::before
pseudo-element to hold our background image. We position it absolutely and give it a negative inset so it's slightly larger than the container, preventing hard edges when it moves. -
animation-timeline: view(block);
: We tie the animation of the::before
element to the visibility timeline of its parent,.parallax-container
, on the vertical (block
) axis. -
@keyframes parallax-scroll
: We animate thetransform
from its defaulttranslateY(0)
to a new position. As the.parallax-container
scrolls through the viewport, the::before
element will move, creating the illusion of depth. You can even use a CSS Custom Property--parallax-speed
to easily control the intensity of the effect!
A Quick Note on Browser Support & Progressive Enhancement
Scroll-Driven Animations are a cutting-edge feature. As of late 2023, they are supported in Chrome, Edge, and Opera. Firefox support is in progress.
This is a perfect use case for progressive enhancement. The animations are "nice-to-haves," not critical functionality. We can use an @supports
query to only apply them in browsers that understand them.
@supports (animation-timeline: scroll()) {
/* Put all your scroll-animation CSS rules in here! */
.progress-bar {
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
.reveal-me {
animation: fade-in-up linear;
animation-timeline: view();
animation-range: entry 0% entry 40%;
}
}
This way, users on supported browsers get a beautiful, animated experience, while users on older browsers see a perfectly functional, static site.
Conclusion
Scroll-Driven Animations represent a monumental leap forward for CSS. They provide a more performant, declarative, and accessible way to create effects that were once the exclusive domain of JavaScript. By offloading this work to the browser's rendering engine, we can build richer, smoother web experiences with cleaner and more maintainable code.
The possibilities are vast, from 3D model rotations on scroll to animated storytelling. So, what will you build with it?
Top comments (0)