Introduction
Learning never stops, just like life itself. It’s been a year since CSS scroll animations were introduced.
However, Safari still lacks support for this feature, as shown in the image below:
I’m done waiting! Many people are already using this new feature in production environments, and I can’t afford to fall behind. Let’s dive in and leave Safari behind for now.
For scroll animations, the required CSS properties go beyond scroll-timeline and view-timeline; you’ll also need the animation-timeline property. This exciting new CSS feature will be introduced in this article as well.
Let’s jump right in!
Use Cases
Many home pages feature a scroll indicator, which represents the percentage of the page scrolled toward the bottom. For example, observe the line at the bottom of the navigation bar in the GIF below:
With native CSS scroll animations now available, implementing a scroll indicator is much simpler.
The code is as follows:
<div class="scroller">
<ins></ins>
<div style="height:400px;"></div>
</div>
.scroller {
height: 200px;
border: 1px solid;
overflow: auto;
scroll-timeline: --indicator;
}
.scroller ins {
display: block;
border-top: 4px solid green;
animation-name: widthExpand;
animation-duration: 1ms; /* Firefox*/
animation-timeline: --indicator;
position: sticky;
top: 0;
}
@keyframes widthExpand {
from { width: 0%; }
to { width: 100%; }
}
At this point, the width of the element in the scroll container changes from 0% to 100% as the scroll progresses, as shown in the GIF recording below.
Differences from Traditional Animation Effects
There are two key differences from traditional CSS animation effects:
First, the scroll-timeline property is used in the scroll container to define a scroll timeline CSS variable.
Second, the animation-timeline property is used on the element that requires animation to assign the animation timeline.
About the animation-timeline Property
The animation-timeline property is also a new CSS property, and its syntax is still complex. Below are some examples of its usage:
/* Single named animation timeline */
animation-timeline: –timeline_name;
/* Single anonymous scroll progress timeline */
animation-timeline: scroll();
animation-timeline: scroll(scroller axis);
/* Single anonymous visibility tracking animation timeline */
animation-timeline: view();
animation-timeline: view(axis inset);
/* Multiple animations */
animation-timeline: –progressBarTimeline, –carouselTimeline;
animation-timeline: none, –slidingTimeline;
Among them, scroll()
determines the animation progress based on the scroll position, while view()
determines the animation progress based on the position of the animated element within the scroll container. It is often used together with the view-timeline property, which will be covered in more detail in a separate chapter.
Animations within the Scrolling Viewport
For example, this common scroll animation effect, where an image zooms in and fades out as it scrolls, can be achieved by using the view-timeline property along with the animation-timeline property, as shown below:
<div class="scroller">
<div style="height:100px;"></div>
<p>first image</p>
<p><img loading="lazy" src="https://raw.githubusercontent.com/trevortylerlee/n1/main/n1.jpeg" /></p>
<p>second image</p>
<p><img loading="lazy" src="https://raw.githubusercontent.com/trevortylerlee/n1/main/n1.jpeg" /></p>
<div style="height:100px;"></div>
</div>
.scroller {
height: 200px;
max-width: 380px;
border: 1px solid;
overflow: auto;
}
.scroller img {
max-width: 100%;
animation: 1ms scaleUp both, 1ms fadeIn both;
animation-timeline: --scaleFade;
view-timeline: --scaleFade;
}
@keyframes scaleUp {
from { transform: scale(0); }
to { transform: scale(1); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
At this point, as the container scrolls, the image will scale and fade in and out based on its position within the scroll viewport, as shown in the GIF below.
If you want to precisely control where the image element starts and ends its animation within the viewport, you can use the new animation-range
CSS property.
However, the learning curve for the animation-range
property is quite steep, so I recommend not diving into it for now.
What if there are animations on elements outside the scroll container?
So far, the two examples presented in this article both show animations on elements inside the scroll container.
But what if you want to animate element A inside the scroll container, while triggering corresponding animations on elements outside the container? Is that possible?
Yes!
You can achieve this by using the timeline-scope CSS property to change the scope of the animation timeline.
Suppose you have a scroll container, and outside the container, there is an image. Here’s a sample HTML code:
<div class="scroller">
<div style="height:400px;"></div>
</div>
<img style="width: 400px;" class="target" src="https://raw.githubusercontent.com/trevortylerlee/n1/main/n1.jpeg" />
body {
timeline-scope: --scaleFade;
}
.scroller {
height: 200px;
border: 1px solid;
overflow: auto;
scroll-timeline: --scaleFade;
}
.target {
animation: 1ms scaleRoate both, 1ms fadeIn both;
animation-timeline: --scaleFade;
}
@keyframes scaleRoate {
from { transform: scale(0) rotate(0deg); }
to { transform: scale(1) rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
In other words, the scope of the scroll animation timeline --scaleFade
has been extended to the body element.
Used to detect whether it is scrollable
The scroll-timeline property also has a very important derived effect, which is detecting whether a div element is overflowing (i.e., its content exceeds the container’s height or width limits). The implementation is as follows:
The container’s overflow is not set to visible.
This way, the container may have a scrollHeight greater than its clientHeight.
Suppose the HTML is as follows:
<section>
<p>content...</p>
<button>load more</button>
</section>
section {
max-height: 120px;
overflow: hidden;
}
button {
display: none;
}
At this point, when the content height exceeds 120px, it is considered overflowing, and CSS can currently detect this. We can then make the “More” button appear.
Detecting scroll overflow
The related CSS code is as follows. It is mostly fixed and can be reused in almost any similar scenario.
section {
--flag: false;
animation: setFlag 1ms;
scroll-timeline: --detectScroll;
animation-timeline: --detectScroll;
}
@keyframes setFlag {
from, to { --flag: true; }
}
@container style(--flag: true) {
button {
display: block;
}
}
The CSS code above is the most valuable part of this article. Once scroll animations are no longer limited by compatibility issues, they should become a key skill for advanced front-end developers.
Implementation Principle
If the container is scrollable, an animation called setFlag will be applied. The only task of the setFlag animation is to reset the CSS variable —flag. Once —flag changes, it will be detected by the container, allowing the styles of the container’s child elements to be freely adjusted.
It resembles a three-level interaction.
A very clever implementation.
There Is Still Much More To Explore.
The scroll detection above can also be directly implemented using animation-timeline: scroll(), which eliminates the need for the scroll-timeline property. However, it can only be applied to the container’s child elements, so an extra HTML element is needed to wrap the content.
The code is largely the same:
<section>
<div class="wrap">
<p>content...</p>
<button>load more</button>
</div>
</section>
.wrap {
--flag: false;
animation: setFlag 1ms;
animation-timeline: scroll();
}
@keyframes setFlag {
from, to { --flag: true; }
}
@container style(--flag: true) {
button { display: block; }
}
In other words, you save a CSS declaration but need an extra layer of HTML, which may not be worth it unless there’s already a container element in the HTML.
Aside from the scroll-timeline property, there’s another related property, view-timeline. The former applies to the entire scroll range, while the latter targets a specific element and often needs to be used with the animation-range property (to control when the animation is triggered).
In conclusion, there is much more to scroll animations than what’s covered in this article.
However, due to compatibility limitations, understanding the few classic cases presented here is sufficient for now.