Effective scroll-driven animations
Scroll driven animations are elements that animate as the user scrolls. By default, scroll-driven animations start and end at the edges of the scrollport or viewport, depending on the animation type. However, this isn't always what you want. Often, effective animations either occur when the element being animated is in the center of it's container or is done animating by the time it reaches that point. In this guide, we look at controlling the start and ends of scroll-driven animation by via @keyframe definitions, animation range properties, and setting insets.
Scroll-driven animation primer
CSS keyframe animations can be linked to scrolling thanks to features defined in the CSS scroll-driven animations module. This module defines methods enabling progressing @keyframes animations along a scroll-progress or view based timeline instead of the default time-based document timeline.
In CSS, animations are created by attaching keyframe animations to an element using the animation-name property (or animation shorthand). The animation moves from the from or 0% keyframe to the to or 100% keyframe based on the animation-timeline. By default, this is the DocumentTimeline, with each animation-iteration taking as long as the time defined by the animation-duration property.
View progress timeline are often used to create scroll reveals, such as fading in images or text sections, animated carousel gallery pages, highlighting nodes in a vertical or horizontal timeline layout as they reach a specific point in the viewport. See the scroll driven animation timeline guide to learn how to create scroll-driven animations that run on a scroll-progress or view-progress timeline, which is driven by the scrolling of an element's contents, rather than the passing of time.
Do use CSS, instead of JavaScript, to create view progress animations as tying an element's entry and exit from the scroll container's viewport, or scroll port using the view() function as the value of the animation-timeline property is more performant than using JavaScript's Intersection Observer API.
Scroll progress timelines
With scroll progress timeline, the timeline progresses based on the scrolling of the scroller either horizontally or vertically.
In this example, we have directions to and from two monuments, with a fake map between them that we want to animated in from fully transparent and scaled down to fully opaque and full size as we scroll down the page.
To create an animation effect, we need an animation. We define a keyframe animation that makes the element on which it is applied go from fully transparent and scaled down, to fully opaque and at it's default size:
@keyframes someChangeEffect {
0% {
opacity: 0;
scale: 0;
}
100% {
opacity: 1;
scale: 1;
}
}
We apply the animation and a scroll timeline to the element we want to animate:
.animatedElement {
animation: someChangeEffect 1ms linear;
animation-timeline: scroll();
}
Scroll down from the first set of directions to the second set, and you'll notice the animated element appearing as you scroll. You may note the main problem with the animation: the element is only fully opaque at full size when it exits the screen. Let's fix this!
Controlling insets with @keyframe selectors
Because the 100% is generally reached when the element leaves the viewport, you likely want to set the final effect of your animation in a keyframe block that occurs well before the end of the animation. You can set your completed effect within the 20%, 50%, or 80% keyframe block rather than using the to or 100% keyframe to ensure the element finishes animating while still in view.
To make the map element full size and fully visible earlier, and then have it reverse the animation as we scroll past the element and have it begin fading out as it reaches the top of the scroll port, we change the keyframe selector values. Here we set the element to be fully visible 20% and stay visible through 80%, before fading out by changing the selector for the hidden state to 0%, 100% and the selector for the visible state to 20%, 80%, :
@keyframes someChangeEffect {
0%,
100% {
opacity: 0;
scale: 0;
}
20%,
80% {
opacity: 1;
scale: 1;
}
}
When the element comes into view as you scroll down the page, the map-like element animates in, reaching it's full size 20% of the way through the scroll port and starts fading out when it reaches 80%. Unfortunately, this creates a really fast fade out. In addition, this method requires redefining your keyframe animations, and may necessitate multiple similar animation definitions that create the same effect but at different points in the scrolling. Fortunately, there are other solutions.
Controlling insets with animation-range
By default, the position in the scroll range is converted into a percentage of progress — 0% at the start and 100% at the end. This animation range can be controlled via the animation-range properties. The animation-range property is shorthand for animation-range-start and animation-range-end, in that order. It is used to set the start and end of an animation's attachment range along its timeline, i.e., where along the timeline an animation will start and end.
.animatedElement {
animation: someChangeEffect 1ms linear;
animation-timeline: scroll();
animation-range: 20% 80%;
}
View progress timelines
You can also progress an animation based on the change in visibility of an element inside a scroller — this is done via view progress timelines. Instead of tracking the scroll offset of a scroll container, view progress timelines track the relative position of an element, called the subject, within a scrollport. The progression of an animation's keyframes is based on the visibility of the subject inside the scroller. Unlike scroll progress timelines, with view progress timelines, you can't specify the scroller — the subject's visibility is always tracked within its nearest ancestor scroller.
A view progress timeline animation only occurs when the element is visible within its scrollport. Timeline progress starts at 0% when the tracked subject starts intersecting the scrollport at the block or inline end edge. The 100% occurs when the subject exits the scrollport at the block or inline start edge.
Because the 100% is generally reached when the element leaves the viewport, you likely want to set the final effect of your animation in a keyframe block that occurs well before the end of the animation. You can set your completed effect within the 20%, 50%, or 80% keyframe block rather than using the to or 100% keyframe to ensure the element finishes animating while still in view.
With view progress timelines, you can adjust the view progress visibility range.
Use view-timeline-inset, part of the view-timeline shorthand, to adjust when the subject is considered to be in view. The default value is auto. The effect of any non-auto inset value is as if you moved the edges of the scroll port: a positive inset value creates an inward adjustment, and a negative value creates an outward adjustment.
Similar to scroll progress timelines, the view progress timeline can be named or anonymous.
Named view progress timeline
A named view progress timeline is one where the subject is explicitly named using the view-timeline-name property, a component of the view-timeline shorthand. The <dashed-ident> name is then linked to the element to animate by specifying it as the value of that element's animation-timeline property.
With named view progress timelines, the element to animate does not have to be the same as the subject. In other words, the element controlling the timeline doesn't have to be the same as the element being animated. This means you can animate one element based on another element's movement within its scrollable container.
Here we use the view-timeline-name property to name an element, identifying the element itself as the source of a view progress timeline. We then set that name as the value of the animation-timeline property.
.item {
animation: action 1ms linear;
view-timeline-name: --a-name;
animation-timeline: --a-name;
}
We applied the animation before the animation timeline, as the animation resets the animation-timeline to auto.
The animation is slightly different from the previous examples in that the spinning effect starts at 20% and ends at 80% of the way through the animation; this means the element will not be actively spinning when it first comes into view and will stop spinning before it is completely out of view.
@keyframes action {
0%,
20% {
rotate: 45deg;
}
80%,
100% {
rotate: 720deg;
}
}
Scroll the element into view. Note that the element animates through the @keyframes animation as it moves through the visible area of its ancestor scroller.
Anonymous view progress timeline: the view() function
Alternatively, a view() function can be set as the value of the animation-timeline property to specify that an element's animation timeline is an anonymous view progress timeline. This causes the element to be animated based on its position inside its nearest parent scroller.
The view() function creates a view timeline. You attach the timeline to the element you want to animate using the animation-timeline property. The function creates a view timeline for each element matched by the selector.
In this example, we again define the animation before the animation-timeline, so the timeline is not reset. We then include an argument-less view() function. We don't specify a scroller, as, by definition, the subject's visibility is tracked by its nearest ancestor scroller.
.item {
animation: action 1ms linear;
animation-timeline: view();
}
Parameters of the view() function
The view() function takes up to three optional values as arguments:
- Zero or one
<axis>parameters. If set, this specifies the scroll axis along which the animation progresses. - Either the keyword
autoor zero, one, or two<length-percentage>inset values. If set, these values specify offsets for the scrollport start and/or end.
Declaring view() is equivalent to view(block auto), which defines block as the axis of the parent element that supplies the timeline and the scroll-padding, which generally defaults to 0, as the insets within the visible area at which the animation starts and ends.
The function sets the values of the view-timeline-axis and view-timeline-inset properties.
The view-timeline-inset arguments specify insets (if positive) or outsets (if negative) that adjust the start and end of the scrollport. They are used to determine the scroll positions at which the element is considered "in view", which determines the length of the animation timeline. In other words, instead of starting at the start edge and ending at the end edge of the scrollport, the animation occurs at the start and end of the inset-adjusted view.
Unlike the scroll timeline's scroll() function, there is no <scroller> argument in the view() function, as the view timeline always tracks the subject within its nearest ancestor scroll container.
In this example, as we are using inset values, we can use the from and to keyframe selectors.
@keyframes action {
from {
rotate: 45deg;
}
to {
rotate: 720deg;
}
}
.item {
animation: action 1ms linear;
animation-timeline: view(block 20% 20%);
}
Accessibility concerns
As with all animations and transitions, always take any user's prefers-reduced-motion preference into account.
Removing an animation's timeline
Setting animation-timeline: none disassociates the element from all animation timelines, including the default time-based document timeline, meaning the element will not animate. While some animations may be necessary, you can remove animations based on the user's prefers-reduced-motion setting with:
@media (prefers-reduced-motion: reduce) {
.optionalAnimations {
animation-timeline: none;
}
}
Because the animation shorthand sets the animation-timeline to auto, use a selector with enough specificity to ensure your animation-timeline isn't overridden by your animation shorthand declarations.