One thing many fancy, award-winning websites have in common is they contain animations triggered by scrolling. Whether it’s a parallax effect, a progress bar animation, elements animating into view, or a full scrollytelling experience, on-scroll effects celebrate the medium of the web, clearly differentiating it from print. Indicating the progress of scroll can also be helpful to users.
Up until now, JavaScript has been a requirement for building scroll-driven animations. But the new Scroll-driven Animations specification enables us to link the progress of an animation to the progress of scroll, all with just CSS. It’s a Christmas miracle!
Simple use: Animated progress indicator
#At its simplest, the animation-timeline
property lets us link any keyframe animation to the progress of scroll. For the first demo, let’s create a festive progress indicator by sliding Santa on his sleigh from the left to the right of the screen.
First, we’ll create the keyframe animation. We’ll use a calc
function so that Santa moves to the end of the viewport but not beyond:
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(calc(100vw - 100%)); }
}
Let’s write some CSS to position an <img>
element with a class of santa
at the bottom of the page.
.santa {
position: fixed;
bottom: calc(100vh - 100%);
left: 0;
}
We can apply the animation to our element using animation-timeline
, with the scroll() function:
.santa {
/* ... */
animation-name: slide;
animation-timeline: scroll();
}
By default, the scroll()
function will link the animation to the nearest scroll container. For a progress indicator, we’d probably want to make sure our animation is linked to the root
scroll container so that the progress bar reflects the user’s progress through the article. We can specify this in the scroll()
function.
.santa {
animation-name: slide;
/* Update: link to `root` scroll container */
animation-timeline: scroll(root);
}
In addition, we can specify the block
or inline
access for our scroll animation to respond to. The default is block
, but if we wanted it to respond to the inline
axis this is how we’d do it:
.santa {
animation-name: slide;
/* Optional: switch to `inline` axis */
animation-timeline: scroll(root inline);
}
If we scroll the page, we should see Santa slide from left to right as we go.
But right now, the animation doesn’t exactly match the rate of scrolling: at some points, it’s faster than others.
The default value for animation-timing-function
is ease
, meaning animations will start slowly, speed up in the middle, and slow down towards the end.
We can set a value of linear
to ensure the progress of our animation matches the progress of scroll.
.santa {
animation-name: slide;
/* Added timing function */
animation-timing-function: linear;
animation-timeline: scroll(root);
}
We can simplify this with the animation
shorthand.
.santa {
/* Update to use `animation` shorthand */
animation: slide linear;
animation-timeline: scroll(root);
}
Parallax images
#How about something a little more exciting than a progress bar?
We can use exactly the same technique to add a parallax effect to a web page. This keyframe animation translates an element on the y-axis
. We can use a custom property (--translate
) to make it easy to adjust the amount our image moves.
:root {
--translate: 200px;
}
@keyframes parallax {
from { transform: translateY(calc(var(--translate) * -1)); }
to { transform: translateY(var(--translate)); }
}
Here we will use the prefers-reduced-motion
media query to ensure that users who have indicated a preference for reduced motion at the system level will see the regular static images.
:root {
--translate: 200px;
}
/* Position <img> element */
.recessed-image {
position: absolute;
width: 100%;
height: 100%;
inset: 0;
object-fit: cover;
}
/* Animation styles only applied for users with no preference for reduced motion */
@media (prefers-reduced-motion: no-preference) {
.recessed-image {
height: calc(100% + var(--translate));
animation: parallax linear;
animation-timeline: scroll(root);
}
}
You can see the parallax effect in action in the demo. As we scroll the page, the hero image appears to move at a slower rate than the text content, giving it a three-dimensional feel. As a bonus, we’re also applying a parallax effect to the other images in the article, by translating them in the opposite direction.
Multi-layer parallax
#We can extend this parallax effect further and create multi-layered backgrounds that respond to scroll!
Let's position three elements on top of each other at the top of our page.
/* Setting a minimum height ensures our page is scrollable! */
body {
min-height: 300vh;
}
/* Position the three backgrounds */
.background,
.mid,
.foreground {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
}
We’ll set a custom property for each one. We want the elements in the foreground to move the most and the ones in the background to move the least, so we’ll give the foreground element the highest value and the background the lowest:
.background {
--translate: 100px;
}
.mid {
--translate: 250px;
}
.foreground {
--translate: 500px;
}
Now, we can apply the animation to our elements like in the previous example.
@media (prefers-reduced-motion: no-preference) {
.background,
.mid,
.foreground {
height: calc(100% + var(--translate));
animation: parallax linear;
animation-timeline: scroll(root);
}
}
@keyframes parallax {
from { transform: translateY(calc(var(--translate) * -1)); }
to { transform: 0; }
}
The custom properties ensure each layer moves at a different rate, giving our page the illusion of depth.
Non-ancestor scroll containers
#If we want to link an animation to the scroll progress of a non-ancestor element, we’ll need a couple of new properties:
scroll-timeline
(shorthand for scroll-timeline-name
and scroll-timeline-axis
) is defined on the scroll container. Here, we specify a timeline name using custom property syntax and the inline
or block
axis (the default is block
).
.scroll-container {
overflow-y: scroll;
scroll-timeline: --progress block;
}
Let’s adjust the example in the previous demo so that the parallax background animation is tied to the progress of a fixed scroll container, which is not an ancestor of the elements we’re animating. Instead of using the scroll()
function for animation-timeline
, we need to use the name of our timeline:
.background,
.mid,
.foreground {
animation: parallax linear;
animation-timeline: --progress;
}
Finally, we must set the timeline-scope
property on a common ancestor of our scroll container and the elements we’re animating. In this case, we can use the <body>
:
body {
timeline-scope: --progress;
}
Now, scrolling the fixed scroll container triggers the animation.
Browser support
#Scroll-driven animations are currently only supported in Chromium, so you’ll need to ensure you provide fallbacks for users whose browsers don’t yet support them. Consider using feature queries to only deliver animation-dependent styles to supporting browsers:
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: scroll()) {
.recessed-image {
height: calc(100% + var(--translate));
animation: parallax linear;
animation-timeline: scroll();
}
}
}
View progress timelines
#Also, as part of the Scroll-driven Animations specification, view progress timelines enable us to link an animation to the progress of an element through the scrollport. This means we can trigger animations when elements enter and exit the viewport.
In the case of our second demo, where we’re animating images throughout the page, it might be more desirable to link the animations to the progress through the viewport rather than the scroll position of the entire page.
To create an anonymous view progress timeline, we can simply use the view()
function in place of the scroll()
function:
.some-element {
animation: appear linear;
animation-timeline: view();
}
@keyframes appear {
0% {
opacity: 0;
transform: translateY(100px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
We can also specify an animation-range
to control when the animation should start and end in relation to its position in the scrollport.
For example, if we want to specify that an element should begin its animation as soon as it begins to enter the viewport and complete the animation once the entire element is in view, we can declare the animation range like this:
.content {
animation: appear linear;
animation-range: entry 0% entry 100%;
animation-timeline: view();
}
As with scroll progress timelines, it’s also possible to create a named view progress timeline using the view-timeline
property.
Resources
#Discover more about scroll-driven animations!
- Scroll-driven Animations specification
- Bramus CSS Day talk (video) by Bramus Van Damme
- Introduction to scroll-driven animations by Bramus Van Damme
- Scroll progress animations in CSS by Michelle Barker
Michelle selected Unicef UK for an honorary donation of $50
The UK Committee for UNICEF (UNICEF UK) raises funds for UNICEF’s emergency and development work for children. We also promote and protect children’s rights in the UK and internationally. We are a UK charity, entirely funded by supporters.