Thanks to the Scroll-Driven Animations Level 1 Specification it is now possible to drive CSS/WAAPI animations by scroll. Not included in that spec are Scroll-Triggered Animations: animations that run when you reach a certain scroll offset.
However, when you combine Scroll-Driven Animations with Custom Properties, Style Queries, and Transitions you can hack your way into creating Scroll-Triggered Animations.
~
# The Code
If you’re here for just the code, here it is. You can see this code in action in the demo.
@keyframes flipthebit {
from {
--bit: 0;
}
to {
--bit: 1;
}
}
:has(> .revealing-image) {
display: block;
animation: flipthebit linear both;
animation-timeline: view();
animation-range: contain 25% contain 25%;
}
@container style(--bit: 0) {
.revealing-image {
opacity: 0;
clip-path: inset(0% 60% 0% 50%);
}
}
@container style(--bit: 1) {
.revealing-image {
opacity: 1;
clip-path: inset(0% 0% 0% 0%);
}
}
.revealing-image {
transition: all 0.5s ease-in-out;
}
If you want to know how it works, read on …
~
# How it works
The code is pretty big, and is a few things working together:
- A Scroll-Driven Animation which toggles a Custom Property
--bit
between0
and1
- A Style Query responding to that Custom Property, applying different styles based the Custom Property value
- CSS Transitions to “animate” between the two states.
Let’s go over each part individually …
~
1. The Animation
@keyframes flipthebit {
from {
--bit: 0;
}
to {
--bit: 1;
}
}
:has(> .revealing-image) {
display: block;
animation: flipthebit linear both;
animation-timeline: view();
animation-range: contain 25% contain 25%;
}
The animation is a set of keyframes flipthebit
that change a custom property from 0
to 1
. It is linked to View Timeline (animation-timeline: view();
) tracking the parent element of the element that needs to animate.
To trigger the animation at a certain point in space, the animation-range
is set. Its start and end are the same, as the bit flipping needs to happen in an instant.
~
2. The Style Query
@container style(--bit: 0) {
.revealing-image {
opacity: 0;
clip-path: inset(0% 60% 0% 50%);
}
}
@container style(--bit: 1) {
.revealing-image {
opacity: 1;
clip-path: inset(0% 0% 0% 0%);
}
}
With the --bit
flipping between 0
and 1
, the animation target can respond to that using a Style Query. When the value is 0
, one set of styles apply. When the value is 1
, different styles apply.
Basically the styles for style(--bit: 0)
are the from
styles, and the styles for style(--bit: 0)
are the to
styles.
UPDATE: Reader Ana Tudor pointed out you can do without a Style Query, by using --bit
directly into calc()
to get the result.
I’ve described this Binary Custom Properties technique before but must admit I’m not a big fan of it as it makes the code harder to read and doesn’t play nice with non-numeric values. Yes, you could move on to using Space Toggles in the latter case, but then again the code still is complicated to read/understand.
~
3. The Transition
.revealing-image {
transition: all 0.5s ease-in-out;
}
To create the illusion of an animation, CSS Transitions are used. The set transition-duration
of 0.5s
is used to determine the “animation”’s duration.
~
# Demo
Here’s a demo that has it all together. If you don’t see any animation, that’s because your browser doesn’t support all necessary requirements. Check the Browser support section to see which browsers are supported.
See the Pen Scroll-Triggered Animations with Scroll-Driven Animations, Style Queries, and Transitions by Bramus (@bramus) on CodePen.
~
# Known limitations
While this approach works, it’s kinda nasty and has a few limitations that I see:
- These are no true animations but transitions.
- It requires a parent element to hook the scroll-driven animation onto.
- Requires Style Queries with Custom Properties to be implemented as well.
- The animations also run in reverse when scrolling back up. This is not always feasible.
Most likely there are more limitations which I’m currently overlooking.
~
# In Closing
This was a fun experiment to do. However, it’s only an experiment and to me makes the case that we still need proper Scroll-Triggered Animations in the future – maybe something to work one for scroll-animations-2
? 😉
~
# Browser support
This is supported by all browsers that support Scroll-Driven Animations and Style Queries. Currently, at the time of writing, this are only Chromium-based browsers (Google Chrome, Microsoft Edge, Brave, Arc, …) versions 115 and up.
~
# Spread the word
To help spread the contents of this post, feel free to retweet its announcement tweet:
Creating Scroll-Triggered Animations by combining Scroll-Driven Animations, Custom Properties, Style Queries, and Transitions
🏷️ #CSS pic.twitter.com/GdxSAw0swb
— Bram.us (@bramusblog) June 15, 2023
~
🔥 Like what you see? Want to stay in the loop? Here's how:
Geat way and much cleaner than the same with JS.
But without the support of FF the usage makes no sense for me 😐
Is it possible to also trigger a js event when the fade in happens?
You could try and pipe it into https://brm.us/style-observer to get notified about the changed value.