Solved by CSS Scroll-Driven Animations: Detect if an element can scroll or not

Because Scroll-Driven Animations are only active when there is scrollable overflow, it is possible to use them as a mechanism to detect if an element can scroll or not. Mix in a Space Toggle or a Style Query, and you’ve got all you need to selectively style an element based on it being scrollable or not.

~

The Code

If you’re here for just the code, here it is:

@keyframes detect-scroll {
  from, to { --can-scroll: ; }
}

.container {
  animation: detect-scroll linear;
  animation-timeline: scroll(self);

  --bg-if-can-scroll: var(--can-scroll) lime;
  --bg-if-cant-scroll: red;
  background: var(--bg-if-can-scroll, var(--bg-if-cant-scroll));
}

If you want to know how to use it and why this works, keep on reading 😉

~

Active vs Inactive Scroll-Driven Animations

A Scroll-Driven Animation is an animation that is driven by scroll. But what if there is no scroll distance to animate on, what happens then? Well, the spec has this covered, and states that the animation is inactive in that case:

If the source of a ScrollTimeline is an element whose principal box does not exist or is not a scroll container, or if there is no scrollable overflow, then the ScrollTimeline is inactive.

Take this animation anim for example, which animates the color from hotpink to lime.

.container {
  height: 250px;
  width: 250px;
  overflow-y: auto;

  animation: anim linear;
  animation-timeline: scroll(self);
}

@keyframes anim {
  from { color: hotpink; }
  to { color: lime; }
}

In a scroll container that has no scrollable overflow, the animation won’t be active, and thus the text will have the color that was declared on it.

See the Pen
Scroll Detection with CSS Scroll-Driven Animations: Intro
by Bramus (@bramus)
on CodePen.

~

Adding Custom Properties to the mix to make a scroll detector

Just like regular CSS Properties, Custom Properties can also be animated.

@keyframes anim {
  from { --foo: 0; }
  to { --foo: 1; }
}

When attached as a Scroll-Driven Animation to a scroll container that has no scrollable overflow, the value of the custom property would be initial, because it is not set.

To turn this into a scroll detector, it’s a matter of making sure the value has one and the same value when inside the animation. This is done by setting the to and from to the same value.

@keyframes anim {
  from, to { --can-scroll: 1; }
}

Hooking that animation to a Scroll-Driven Animation and also making sure that --can-scroll has an initial value of 0, the full code becomes this:

.container {
  height: 250px;
  width: 250px;
  overflow-y: auto;

  --can-scroll: 0;
  animation: detect-scroll;
  animation-timeline: scroll(self);
}

@keyframes detect-scroll {
  from, to  {
    --can-scroll: 1;
  }
}

For elements that have scrollable overflow, the animation will be active, so the computed value of --can-scroll will be 1. For elements without scrollable overflow, the value will be 0

This value can be used in calculations, for example:

.container {
  outline: calc(10px * var(--can-scroll)) dotted lime;
}

This code will give the scrollable container a 10px side dotted lime outline, whereas a non-scrollable container will have a 0px wide outline.

See the Pen
Scroll Detection with CSS Scroll-Driven Animations
by Bramus (@bramus)
on CodePen.

Note that you don’t need to register the property using @property here, as there’s no interpolation that needs to be done. The property is simply used as a telltale.

~

Making it more developer friendly

To make it easier to work with, there’s a few variations to make:

  • Use a Space Toggle
  • Use a Style Query

Space Toggle Variant

The Space Toggle Variant follows the basic mechanics of a Space Toggle by setting the --can-scroll to an initial value of initial and a space as the value inside the animation. This allows you to set more than just numbers.

.container {
  height: 250px;
  width: 250px;
  overflow-y: auto;
	
  --can-scroll: initial; /* initial = false */
  animation: detect-scroll;
  animation-timeline: scroll(self);
	
  --color-if-can-scroll: var(--can-scroll) lime;
  --color-if-cant-scroll: red;
  outline: 10px dotted var(--color-if-can-scroll, var(--color-if-cant-scroll));
}

@keyframes detect-scroll {
  from, to {
    --can-scroll: ; /* space = true */
  }
}

A downside of this approach is that is a bit harder to read if you’re not entirely familiar with how Space Toggles work.

See the Pen
Scroll Detection with CSS Scroll-Driven Animations
by Bramus (@bramus)
on CodePen.

Style Query Variant

The Style Query Variant uses a Style Query to respond to this change in value. This makes it act as a polyfill for a State Query.

@container style(--can-scroll: 1) {
  p {
    color: lime;
  }
}

@container style(--can-scroll: 0) {
  p {
    color: red;
  }
}

A downside of this approach is that is that you can only style child elements of the container and that – at the time of writing – only Chrome supports Style Queries.

See the Pen
Scroll Detection with CSS Scroll-Driven Animations (Space Toggle)
by Bramus (@bramus)
on CodePen.

~

Practical example

Recently I saw this nice demo by Shu Ding that shows/hides scroll indicators as you scroll up/down, powered by Scroll-Driven Animations.

See the Pen
Scroll Timeline
by Shu Ding (@shuding)
on CodePen.

While nice, there is an issue with it though: when the content is too small for the scroller, the indicators both show!

See the Pen
Scroll Timeline
by Bramus (@bramus)
on CodePen.

This is where the CSS scroll-detection detailed in this post can help: only show the indicators when there is scrollable overflow. I do this by conditionally setting the visibility to hidden so that there’s no layout shift when they are not visible.

@keyframes detect-scroll {
  from, to { --can-scroll: ;}
}

.container {
  animation: detect-scroll;
  animation-timeline: --scroll-timeline;
}

.up, .down {
  --visibility-if-can-scroll: var(--can-scroll) visible;
  --visibility-if-cant-scroll: hidden;
  visibility: var(--visibility-if-can-scroll, var(--visibility-if-cant-scroll));
}

The result, looks like this:

See the Pen
Conditional Revealing Scroll Indicators Scroll-Timeline
by Bramus (@bramus)
on CodePen.

While at it, I also made the code more reusable. Instead of limiting the reveal keyframes from 0% to 2% – which makes them depend on the containing block’s size – they span the full 0%100% range. Then, animation-range: 20px 40px; is used to limit when the animation should run. See this thread on X (née Twitter) for more info.

~

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

Join the Conversation

9 Comments

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.