Scroll-Driven Animations can be used for more than driving an animation by scroll. In this post, I share how you can use Scroll-Driven Animations to fake a :snapped
selector – a fictitious selector that matches elements that are currently snapped within their scroll-snapping enabled ancestor scroller.
~
# Intro
One of the cool and unexpected things of the upcoming Scroll-Driven Animations feature – coming in Chrome 115, which is released this July – is that it can be used well beyond its original intended use when combined with other CSS features.
Roma(n) Komarov for example has used them to apply styles when an element with sticky positioning is stuck and even created text that shrinks to the available width with it. Or take Johannes Odland’s scroll-persisted state, which is quite unexpected. Or even my own Scroll-Triggered Animations hack that’s built upon it.
In one of Johannes’s toots, the :snapped
selector came up. It’s a fictitious selector that would match the element that is currently snapped within its parent scroller that has scroll-snapping applied to it. As you might have guessed already, that one too can also be faked with Scroll-Driven Animations.
~
# The Code
If you’re here just for the code, here it is. It is used in the demo below.
@keyframes snapped {
to {
/* Declare your :snapped styles here */
}
}
[data-snap-align] {
scroll-snap-type: x mandatory;
}
[data-snap-align] > * {
animation: snapped steps(1, start);
animation-timeline: view(inline);
}
[data-snap-align="start"] > * {
scroll-snap-align: start;
animation-range: exit -1px exit 1px;
}
[data-snap-align="center"] > * {
scroll-snap-align: center;
animation-range: cover calc(50% - 1px) cover calc(50% + 1px);
}
[data-snap-align="end"] > * {
scroll-snap-align: end;
animation-range: entry calc(100% - 1px) entry calc(100% + 1px);
}
~
# How it works
At the core sites a scroll-driven animation that is linked to each element using a View Timeline. In the to
keyframe block, the styles that need to apply when the element snapped are declared.
@keyframes snapped {
to {
/* Declare your :snapped styles here */
}
}
[data-snap-align] {
scroll-snap-type: x mandatory;
}
[data-snap-align] > * {
animation: snapped steps(1, start);
animation-timeline: view(inline);
}
Depending on the scroll-snap-align
property value (start
, center
, or end
), the animation-range
must also be set to a different value. The thing that might trip you up here is that the start
of scroll-snapping maps to the exit
range of Scroll-Driven Animations. It feels like they are opposites, but in fact they are not:
scroll-snap-align: start
= right before the element is about to exit the scroller = aroundexit 0%
scroll-snap-align: center
= when the element is at the center of the scroller = aroundcover 50%
scroll-snap-align: end
= when the element has entered the scroller completely = aroundentry 100%
Because you can’t set the start and end range to the same value – the animation would not run in that case – you need to give it some space to run. You do this by adding/subtracting 1px
from the range-start and range-end. For the elements with scroll-snap-align: end;
, for example, the range becomes this:
[data-snap-align="end"] > * {
scroll-snap-align: end;
animation-range: entry calc(100% - 1px) entry calc(100% + 1px);
}
Finally, to prevent elements from being animated midway – which can happen as I’ve noticed – don’t use a linear
timing function but use a steps
-based one.
[data-snap-align] > * {
animation: snapped steps(1, start);
animation-timeline: view(inline);
}
~
# Demo
Try out the code shown above in the pen below. Note that your browser needs to support Scroll-Driven Animations for it to work, which is Chrome 115 at the time of writing.
See the Pen (Ab)using Scroll-Driven Animations to fake Scroll-Snapping :snapped by Bramus (@bramus) on CodePen.
If your browser does not support Scroll-Driven Animations, you can see a recording embedded at the top of this post.
~
# It’s not perfect
While the code allows you mimic what a hypothetical :snapped
selector would give you, it’s not perfect:
- The state applies before the scroll has actually snapped. If you scroll quickly, you can see some items apply the animation even though the scroller is still scrolling. Same if you scroll slowly across a point where it should snap.
- It’s implemented using animations. This can have some unwanted side-effects because animations are a separate origin in the cascade.
- It requires scroll-driven animations.
You could work around some of these with some extra JavaScript that listens for the scrollend
event and/or make it a Scroll-Triggered Animation, but still it would remain a hacky way to achieve all this. To me, this exactly makes the case to eventually have a proper way to to apply styles onto snapped elements built straight into CSS.
~
# Spread the word
To help spread the contents of this post, feel free to retweet its announcement tweet:
Faking a `:snapped` selector with Scroll-Driven Animations
🏷️ #CSS #ScrollDrivenAnimations #ScrollSnapping pic.twitter.com/AgXRmQ5cKh
— Bram.us (@bramusblog) June 26, 2023
~
🔥 Like what you see? Want to stay in the loop? Here's how: