Solved by CSS Scroll-Driven Animations: A :snapped selector

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 = around exit 0%
  • scroll-snap-align: center = when the element is at the center of the scroller = around cover 50%
  • scroll-snap-align: end = when the element has entered the scroller completely = around entry 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:

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.