An outlined heart

Animating elements while a user scrolls the page is a popular practice on the web. With scroll-timeline and the scroll function available in Chrome Canary, we get a nice preview on how these scrolling animations in pure CSS will work. The perfect time to create some demos and play around with it.

Animating elements while a user scrolls the page is a popular practice on the web. With scroll-timeline and the scroll function available in Chrome Canary, we get a nice preview on how these scrolling animations in pure CSS will work. The perfect time to create some demos and play around with it.

If you want to try the demo’s, you should open them in Chrome Canary for now, or if you’re reading this and Chrome 115 is released, you might be good by just using that! :)

It’s time for some new properties again! Nothing too complicated to start with. Let’s take the following animation into account:

.animate-on-scroll {
  animation: fadeIn ease-out;
}

@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

If you want to make this CSS animation run only when a user scrolls the page. Just add the following:

.animate-on-scroll {
  animation-timeline: scroll(root);
}

That was easy right? A few things are happening now. The percentage of our keyframes is the same percentage that the user scrolls. So in this case we are fading something from 0 to 1 over the whole length of a page scroll.

But what is this scroll() function we see here? This is something new and it’s very exciting.

The scroll function

The scroll function can have two values inside of it. The first part specifies whether the scroll port we’re targeting is the root scroller or the nearest ancestor. We can also specify the scroll direction in which the animation should occur. Options are: horizontal, vertical and logical variants block and inline. The default of this function is the following:

.animate-on-scroll {
  animation-timeline: scroll(block nearest);
}

It’s a really handy feature, especially for targeting the root scroller of a page. Maybe we want to show a little “scroll to top”-button when the user scrolls about 30% of the page’s length. This could be done easily without using JavaScript:

<main id="top">
  <!-- some content here -->
  <a href="#top" class="scroll-to-top"> Scroll to top </a>
</main>

And we can reveal our scroll to top button while scrolling simply by doing the following:

.scroll-to-top {
  animation: revealScroller ease-out;
  animation-timeline: scroll(root);
}

@keyframes revealScroller {
  0%,
  30% {
    opacity: 0;
    transform: translateY(100%);
  }
  38%,
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

When the user scrolled 30% of the page, the button will reveal itself and will be fully visible by the time the user scrolled 38% of the page. Here is the demo of this:

I had a lot of fun playing around with this. Another useful example that many websites seem to have is a progress bar to indicate how much you’ve read of a certain article. I created my own little version of this with a little “chicky” walking on your screen while scrolling, be warned though, it has a bit of keyframe overload:

The downside is that my animation seems to trigger a bit of moonwalking when scrolling back to top. Let’s just call it a feature for now, ok? 😉

Naming a scroll-timeline and scoping the parent

if the animated element doesn’t need to interact with the nearest ancestor or root scroll but rather with a scroll happening somewhere else on our page, we have the ability to target a custom scroll-timeline on the item we want to scroll and add a timeline-scope to the wrapping parent. As a simple example, take a look at the following HTML:

<section>
  <div class="list">
    <ul>
      <li>Ducks, geese, and waterfowl</li>
      <li>Pheasants, grouse, and allies</li>
      <!-- etc etc etc -->
    </ul>
  </div>
  <!-- item we want to animate -->
  <div class="animation"></div>
</section>

In this example, .list has an overflow-y: auto; and a fixed height. When we scroll in our .list we want .animation to do something. In this case, .animation isn’t a direct child, but we need to start with creating a custom scroll-timeline so we will use a scroll-timeline-name to name our .list:

.list {
  scroll-timeline: --listTimeline block;
}

scroll-timeline is a shorthand for:

  • scroll-timeline-name: provides a name to the selector
  • scroll-timeline-axis: The direction of the scroll: horizontal, vertical, block or inline.

Unfortunately, just adding this won’t be enough because our .animation isn’t a child of the element we’re scrolling, we’ll need some way to let the wrapping parent know that these belong in the same scope, this is where timeline-scope comes in handy. So let’s update the CSS based on our HTML example:

Please note that the timeline-scope will be a bit later to the party as this will be released in version 116.

section {
  timeline-scope: --listTimeline;
}

.list {
  /* other scroll snap related code */
  scroll-timeline: --listTimeline block;
}

By naming this list and scoping the wrapping parent we can now bind our .animation selector by declaring the animation-timeline again, but now using the name instead of the scroll function:

.animation {
  position: absolute;
  top: 0;
  right: 0;
  animation: moveBackground alternate linear;
  animation-timeline: --listTimeline;
  /* some repeating bg image */
}

@keyframes moveBackground {
  0% {
    background-position: 0 0;
  }
  100% {
    background-position: 0 100px;
  }
}

These are two results of using these techniques. One for the vertical scroll and another for a horizontal scroll:

Checking for support for scroll-timeline

We can use a feature query to enable our animations only for those browsers that support it. It’s a good idea to do this as I noticed that your animation will just fire right away if the feature isn’t supported.

@supports (animation-timeline: scroll()) {
  /* cool scrolly stuff goes here */
}

When using this in our projects, it’s nice to check for user preferences if you’re animating a lot of things on scroll, so check if a user doesn’t want a lot of distractions because of the motion. For those people add the following media feature query:

@media (prefers-reduced-motion) {
  /* cancel the animations here */
}

I didn’t use this in my demo’s, but lately, I always make sure to do this for heavy-animated elements in projects.

Conclusion and final demo

There are still a lot of requests and questions being asked on the CSSWG github and it shows that this is only the beginning. Still, the spec has changed a few times and i’ve updated this article ver since. Personally, I’m wondering if we will be able to target the scroll direction so that my Chicky demo won’t have to moonwalk back into position (So, I asked the question, can’t hurt to try). Maybe there is an answer in state queries or something else. As it turnes out, it seems that this Spec will really remove the need of some heavy libraries such as GSAP. As these CSS animations will run off the main thread (when using transforms), they will be really speedy and running at a whooping 120hz refresh rate. The ability to trigger these little animations and create some extra interaction is a great addition to CSS. But I’m sure many more demo’s will be made soon with people going crazy on these things.

For more information on scroll driven animations, you should checkout scroll-driven-animations.style

And because I’m creating this as my valentine’s day article, let me share some love for the scroll with this extra demo:

 in  css , ux