Let’s make a CSS scroll animation! No frameworks, no JavaScript. Connect user interaction with real-time scroll interaction feedback; helping transition color, position, visibility, and more.
At the time of writing this, it is Chromium only:
Browser Support:
ChromeSupported
FirefoxNot supported
Internet ExplorerNot supported
SafariNot supported
OperaNot supported
I believe scroll animations are perfect for progressive enhancement, but you may consider using a polyfill if it is crucial for them to function across all browsers today.
A humble spinning animation
Let’s begin with something familiar—an infinitely spinning element. It has keyframes that rotate the star five times over five seconds, continuously:
“`html
@keyframes spin {
to {
transform: rotateY(5turn);
}
}
@media (prefers-reduced-motion: no-preference) {
div {
animation: spin 5s ease infinite;
}
}
“`
Try the demo
A scroll driven animation (SDA)
Let’s convert that animation to a scroll-driven animation, if the browser supports it. Add one line of CSS that instructs the animation to be triggered by scrolling, using the scroll() function:
“`html
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: scroll()) {
div {
animation: spin linear both;
animation-timeline: scroll();
}
}
}
“`
Try the demo
What just happened?! To the keyframes we defined, they still run from 0-100%. But now, 0% is the scroll start position and 100% is the scroll end position. You changed the animation-timeline. Notice the addition of both, an animation-fill-mode value that allows the animation to play forwards and backwards. I did add some supporting styles. I applied position: fixed to the image, allowing us to observe its spin as it remains in the viewport. Additionally, I modified the animation easing to linear because, in my experience, scroll-linked animations tend to feel better when linearly connected to my scrolling gesture.
A scroll port intersection driven animation
Next, change scroll() to view() and add one more line of CSS that specifies the animation-range:
“`html
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: scroll()) {
div {
animation: spin linear both;
animation-timeline: view();
animation-range: contain;
}
}
}
“`
Try the demo
What just happened?! To the keyframes we defined, they still run from 0-100% 🤓 But now, 0% is when the element is entering the scroll area and 100% is when it’s about to go out of that scroll area. This view() function powers the animation as it crosses a scrollport. The default value for animation-range is cover, which stinks in this star spinning demo because it makes it hard to distinguish between the scroll() demo; it would be spinning the whole time you see it, as it would spin edge to edge. So I’ve also added animation-range and set it to contain so that the animation is edge to edge within the scrollport. It’s a little goofy looking that it’s so stiff as it enters and exits, but I hope it’s at least super clear what’s happening. Bramus has built a super rad tool that helps visualize the options you can pass into animation-range, definitely worth checking out if you intend to make some CSS scroll animations.
More elements!
We saw one element animate itself across the viewport, but what if multiple elements get assigned the same animation and view() attachment, but enter and exit the scrollport at different times? Let’s see! In this next demo, each image scales up as it enters the scrollport, with individually running animations based on their unique scrollport intersection. Instead of a keyword for the animation-range, this time I show that you can use lengths. In this case, I wanted cute product images to scale up once they were 25vh in and complete the scaling by 75vh of their scrolling area:
Try the demo
The CSS for this effect is just this:
“`html
@keyframes scale-a-lil {
from {
scale: .5;
}
}
@media (prefers-reduced-motion: no-preference) {
figure img {
animation: scale-a-lil linear both;
animation-timeline: view();
animation-range: 25vh 75vh;
}
}
“`
The keyframes set the out state for when scaled down. The animation says scale the images to regular size given the element intersects with the scrollport at the provided animation-range.
More examples!
With those building blocks of scroll attachment, view attachment, and animation ranges, there’s a lot you can do. Let’s have a look at some more examples.
Fade in the primary nav on scroll
This demo has a very clean and minimal initial load where navigation is hidden, but then a little bit of scroll brings it in. It also shows how to animate the navbar shadow on scroll:
Theme scroll
This next demo animates an angle @property CSS variable, which is then used in a cylindrical color space. As the user scrolls, it makes a rainbow effect because the effect changes 0turn to 1turn; encompassing all the hues in the color space:
Pull to refresh with scroll snap
In this demo, the pull-to-refresh icon rotates, and the prompt text fades in as the user scrolls, effectively conveying the result of their gesture:
A more advanced example
Up until now, examples have been with the whole page, and vertical scroll only. In this section, I will show you how to:
– Hookup horizontal scroll() animation
– Link animation with scroll-snap points
– Share scroll() or view() progress with other elements with timeline-scope
Try the demo
Setting up snapping horizontal scroll()
It’s time to pass some parameters to scroll(). It is a function after all 🤓 Attach the animation to horizontal scroll by specifying the axis in the function, such as scroll(x). Ensure that the axis matches the setup of your overflow. If you’re utilizing logical properties, you would use scroll(inline).
“`html
html {
overflow-x: auto;
scroll-snap-type: x mandatory;
animation-timeline: scroll(x);
body > section {
scroll-snap-align: start;
}
}
“`
Now, any animation added to the markup will be controlled by the horizontal scroll of the page.
Animating carousel cards in and out
This is one of the best parts of the code to play with in the demo. It’s the animation as the cards go in and out of the horizontal scroll view:
The keyframes run from 0%-20% as elements “enter stage right”, then hold a natural state until 80% of the scroll area, then “exit stage left” finishing the 80%-100% part of the keyframes. My animation keyframes are like, “be shy as you enter the stage, then try to stick around as you exit the stage.” 😂
“`html
@keyframes fancy-in {
/* card entry */
0% {
transform: translateX(25vw);
}
/* card on stage */
20%, 80% {
opacity: 1;
transform: none;
}
/* card exit */
100% {
transform: translateX(90%) scale(.98);
opacity: 0;
}
}
“`
You should definitely mess with this code. Change the timing percentages or change the transforms.
Next, connect these dynamic keyframes to the scroll intersection on the x-axis for each .card element. Ensure that the animation occurs only if motion is acceptable for the visitor.
“`html
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: scroll()) {
animation: fancy-in linear both;
animation-timeline: view(x);
}
}
“`
If you want to go off the rails and play with what you’ve learned, try making the keyframes just 0%-100% and change the animation-range to get the effect. Or, add the animation ranges into the keyframes. There are options here, and room for you to find a preference as an author and crafter.
Crossfading the next and previous buttons with scroll()
It’s a nice, elegant touch to visually fade the next button when at the end of a carousel, and also fade out a previous button if at the start. We can use CSS to do this! Here are the keyframes, they make this look like it might be simple:
“`html
@keyframes toggle-control {
50% {
opacity: 0;
}
}
“`
The next section is advanced and extremely important when you want to redirect an intersection of one element to power a different element’s animation.
Define observable snap sections
Rather than attempting to fade out the buttons with an estimated animation range for snap sections’ size, each button can observe a specific section and its intersection with the scrollport. This allows the buttons to be perfectly animated as the first and last sections go in and out of view. It’s awesome.
Intrinsically sized snap area animation timelines
Use view-timeline to expose the view() progress of a specific element. Provide a custom name and axis. Soon we’ll make these names more public to other elements, they can reference a section’s view progress to power their own animation.
“`html
#section-1 {
view-timeline: –section-1 x;
}
#section-2 {
view-timeline: –section-2 x;
}
#section-3 {
view-timeline: –section-3 x;
}
#section-4 {
…
“`
Source link