Scroll progress animations in CSS. Learn how to link animations to the scroll progress of a container. A vibrant gradient behind artwork of computer graphic with code and a window with a scrollbar.

Scroll progress animations in CSS

Author avatarMichelle Barker7 minute read

Scroll-linked animations can often add a touch of class to a website, but have long been the preserve of JavaScript. Now a brand new specification is being implemented to enable us to create rich scroll-driven experiences with CSS!

When we think of scroll-driven animations, we generally mean one of two things:

  • An animation that occurs as the user scrolls, with the progress of the animation explicitly linked to the scroll progression. For example, a progress bar for a long article.
  • An animation that occurs on an element as it enters, exits, or progresses through the visible area — often the viewport, but it could be the visible portion of another scrollable container (this is defined as the scrollport).

The Scroll-driven Animations specification covers both these types of animations. In this article, we're going to take a look at Scroll Progress Timeline first, which, as the name suggests, links an animation to the progress of scroll.

Note: The features in this post here have limited browser support at the time of writing. It's best to use Chrome Canary but you can also enable experimental features in Chrome 115 or later to follow along with the examples and have a play around with scroll-linked animations yourself.

Using the animation timeline

In this example, we'll implement a common feature: animating a simple progress bar to scale from left to right as the user scrolls a web page. Because we want to link our animation to the progress of the root scroller, we can use an anonymous scroll progress timeline.

First let's define the animation itself. We want our progress bar to scale from left to right, so we'll use a transform:

css
@keyframes scaleProgress {
  0% {
    transform: scaleX(0);
  }
  100% {
    transform: scaleX(1);
  }
}

To associate our progress bar element’s animation with the progress of scroll, we've used the animation-timeline property and set the scroll() function as its value.

css
.progress {
  animation-timeline: scroll();
}

The scroll() function allows us to specify the scroll container and axis. The default value is scroll(nearest block), meaning that the animation will be linked to the nearest scrollable ancestor on the block axis. This is sufficient for our purposes, although we could optionally specify the root as the scroll container, since we want to explicitly link the animation to the progress of scroll of the viewport.

css
.progress {
  animation-timeline: scroll(root block);
}

Lastly, we need to add our animation to the progress bar element, with our keyframe animation as the animation-name. We need to set the animation duration to auto, as the duration will be determined by the scroll progress. We're also setting the easing (animation-timing-function) to linear so that it progresses smoothly in line with scroll. If we were to use the default value (ease), the animation would start off slowly before rapidly speeding up, then slowing down at the end — not what we want from a progress indicator!

css
.progress {
  animation-timeline: scroll(root);
  animation-name: scaleProgress;
  animation-duration: auto;
  animation-timing-function: linear;
}

We could condense this somewhat using the animation shorthand property:

css
.progress {
  animation: scaleProgress auto linear;
  animation-timeline: scroll(root);
}

Note: animation-timeline is not currently included in the shorthand. However, the animation property resets animation-timeline to auto (the default), so we need to declare animation-timeline after the animation shorthand.

Multiple animations

Just like regular keyframe animations, we can apply more than one scroll timeline animation simultaneously, such as changing the color of our progress bar.

css
.progress {
  animation: scaleProgress auto linear, colorChange auto linear;
  animation-timeline: scroll(root);
}

@keyframes scaleProgress {
  0% {
    transform: scaleX(0);
  }
  100% {
    transform: scaleX(1);
  }
}

@keyframes colorChange {
  0% {
    background-color: red;
  }
  50% {
    background-color: yellow;
  }
  100% {
    background-color: lime;
  }
}

Using different easing functions

Although we deliberately chose a linear ease in the previous example, we can also achieve some interesting effects with steps() easing. This example shows a different kind of progress bar, one that employs discrete steps instead of smooth linear scaling. We're setting a linear gradient background on the progress bar element for the color segments, then animating the clip-path to reveal each one in turn:

css
.progress {
  background: linear-gradient(
    to right,
    red 20%,
    orange 0,
    orange 40%,
    yellow 0,
    yellow 60%,
    lime 0,
    lime 80%,
    green 0
  );
  animation: clip auto steps(5) forwards;
  animation-timeline: scroll(root);
}

@keyframes clip {
  0% {
    clip-path: polygon(0 0, 0 0, 0 100%, 0 100%);
  }
  100% {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
  }
}

Repeating and reversing animations

Scroll progress animations can be used in conjunction with existing animation-direction and animation-iteration-count properties. So we can make our animation repeat a number of times throughout our scroll timeline, or play in reverse. Here the "ball" bounces several times as we scroll.

css
.progress {
  animation: bounce auto linear 6 alternate;
  animation-timeline: scroll(root);
}

@keyframes bounce {
  100% {
    transform: translateY(-50vh);
  }
}

Targeting a non-ancestor scroll container

Sometimes, we might want to animate an element that is not a descendant of the scroll container, but still link that element's animation to the scroll container's progress. In order to do this, we need to create a named scroll progress timeline. We'll declare the timeline's name and axis on our scroll container by using the shorthand scroll-timeline property (shorthand for scroll-timeline-name and scroll-timeline-axis). Again, the block axis is the default. The timeline name must be prefixed with two dashes (similar to custom properties), which ensures that it will not conflict with other property values.

The scroll container must be an element that has the ability to scroll.

css
.scroller {
  max-height: 300px;
  overflow: scroll;
  scroll-timeline: --scale-progress block;
}

We can link the element we want to animate to the scroll timeline using the animation-timeline property.

css
/* Sibling of .scroller */
.progress {
  animation: scaleProgress auto linear;
  animation-timeline: --scale-progress;
}

@keyframes scaleProgress {
  0% {
    transform: scaleX(0);
  }
  100% {
    transform: scaleX(1);
  }
}

Animating an ancestor of a scroll container

This works provided the element we want to animate is a sibling of the scroll container. What if we want to animate an ancestor, or the descendant of a sibling?

We need one more CSS property, timeline-scope, which allows us to modify the scope of a named timeline to include the element on which it is set. If we set this property on the body, for example, we can now animate that element's background color, despite it being an ancestor of the scroll container.

Image

Let's have a look at the code:

css
/* Ancestor element: We want to scope the scroll timeline to include this element and its descendants */
body {
  timeline-scope: --scale-progress;

  /* Apply the animation */
  animation: colorChange auto linear forwards;
  animation-timeline: --scale-progress;
}

/* The scroll container on which we declare our timeline */
.scroller {
  max-height: 300px;
  overflow: scroll;
  scroll-timeline: --scale-progress block;
}

/* Apply the animation on the sibling as before */
.progress {
  animation: scaleProgress auto linear;
  animation-timeline: --scale-progress;
}

Note: timeline-scope is currently only supported in Chrome Canary and Chrome 116 with experimental web platform features enabled.

Exploring creative examples

So far, we've created some fairly basic progress bar animations — perhaps one of the more obvious use cases for scroll progress timelines. But there's nothing stopping us getting creative with our scroll animations.

Horizontal image scroller

Animating elements horizontally while the user scrolls vertically can make a web page feel more dynamic and less linear. Here we're animating a row of images so they slide in from the left as the user scrolls vertically.

See the full example on CodePen

Using a motion path

We can position and animate elements along a path in CSS using offset-path to define a motion path for the element to follow. This is a much more fun way to indicate progress than a rectangular progress bar!

See the full example on CodePen

Combining multiple animations

In this demo, we're animating multiple elements on scroll: the text is revealed, while the box slides and somersaults from left to right. To simplify our code and avoid having to create multiple keyframes, we're animating a custom property and calculating the translateY value using a trigonometric function, which are supported in the latest releases of all major browsers. Unlike transform properties, custom properties are animated on the main thread, which means your website could suffer from poor performance if you're tempted to animate a lot of them.

See the full example on CodePen

Accessibility and users motion preferences

As with any intrusive animation, we should always prioritise accessibility and ensure we turn off animations for those who would rather do without them. This can be particularly important for scroll-driven animations, which can cause feelings of motion sickness even in users who don't generally suffer from vestibular disorders. If you'd like to know more, check out Respecting users' motion preferences to see how to use the prefers-reduced-motion media query to ensure your animations are accessible.

Summary

So, how do scroll timeline animations in CSS compare to JS libraries (once they're universally supported)? If you're creating especially complex animations, you might still need to reach for a library like GSAP, which is especially well-equipped to handle complex orchestration. Libraries may also supply us with features like custom easing, and GSAP's Inertia plugin (which allows an animation to glide to a stop once scrolling has finished, rather than coming to an abrupt halt). At the moment, we don’t have a way to detect whether an element is currently scrolling in CSS.

Likewise if your animation is crucial for user experience, you might need to hold off for now, as it may be some time before scroll-linked animations are universally supported.

If on the other hand, you need a few relatively straightforward scroll-driven animations, CSS could save you (and your users) a big JS payload, giving you a great performance win!

I hope you enjoyed reading the post and exploring the examples. Feel free to leave your feedback, thoughts, or questions on Discord or on GitHub.

Useful resources

Stay Informed with MDN

Get the MDN newsletter and never miss an update on the latest web development trends, tips, and best practices.