
Scroll-linked animations look slick, but until now it often meant writing a fair amount of JavaScript, scroll event listeners, getBoundingClientRect(), class toggling and so on, with UX benefits often offset by performance downsides.
CSS scroll-driven animations are now available natively in all major browsers, so we’ve put together an example to show a couple of great ways to make use of these features.
Scroll-Driven Progress Bar
First, let’s create a scroll-driven progress bar
Our HTML features a simple <div> with the class progress-bar, with all the work being done in our CSS:
@keyframes grow {
from {
width: 0%;
}
to {
width: 100%;
}
}
.progress-bar {
/* ...styling above here, not relevant to our scroll animation */
animation: grow linear;
animation-timeline: scroll();
}
Despite our .progress-bar having other styles, with just a few lines we handle our entire animation.
First, we define a grow animation, animating between 0% and 100% in width.
We then add this to our .progress-bar using the animation property, with linear easing (anything else will produce strange results for this), and then finally define our animation-timeline as scroll(), to say that we should animate between our two keyframes in time with the scroll.
As a side point, scroll() with no arguments defaults to the nearest scrollable ancestor on the vertical axis — which in this case is just the page. You’ll sometimes see scroll(root block) in other examples, which is the explicit version of the same thing, in case you get confused.
Animate-in on Scroll
Secondly, we can use CSS to create a fade-up animation, animating elements as they come into view.
We start off creating a fade-up animation, going from opacity: 0 and 2rem downwards to opacity: 1, in its natural position:
@keyframes fade-up {
from {
opacity: 0;
translate: 0 2rem;
}
to {
opacity: 1;
translate: 0 0;
}
}
Then, like we did with the progress bar, we apply it:
p {
animation: fade-up ease-out both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
It might look like a short of shorthand, but it’s surprisingly straight forward. The view() tracks each element’s own position in the viewport individually, rather than the scroll position of the whole page, so every p gets its own independent animation as it comes into view.
The both value in the animation shorthand is animation-fill-mode: both, which keeps the element in its from state before the animation starts, so nothing pops in before it’s supposed to.
Finally, animation-range: entry 0% cover 40% controls the timing window, to say trigger the moment it enters, and finish by the time it’s 40% of the way across the screen.
Those two functions are the core of the API. scroll() is for “how far have I scrolled?”, view() is for “is this element in view yet?”.
Added Accessibility
Finally, a simple media query makes your website more accessible by allowing those who have prefers-reduced-motion enabled to view an animation-less version:
@media (prefers-reduced-motion: reduce) {
.progress-bar,
p {
animation: none;
}
}
While not every aspect is fully supported just yet (as of publishing scroll() has 82% support) it’s a great feature that’s very simple to use once you’re used to how it works.
We hope you enjoyed this article, and you can find the example over on CodePen and as always share your tips and tricks with us on socials for a shoutout.