Get started with scroll-driven animations in CSS

1. Before you begin

Scroll-driven animations allow you to control the playback of an animation based on the scroll position of a scroll container. This means that as you scroll up or down, the animation scrubs forward or backward. Additionally, with scroll-driven animations you can also control an animation based on an element's position within its scroll container. This allows you to create interesting effects such as a parallax background image, scroll progress bars, and images that reveal themselves as they come into view.

New in Chrome 115 is support for a set of JavaScript classes and CSS properties that allow you to easily create declarative scroll-driven animations. These new APIs work in conjunction with existing Web Animations and CSS Animations APIs.

This codelab teaches you how to create scroll-driven animations using CSS. By completing this codelab, you become familiar with the many new CSS properties introduced by this exciting feature, such as scroll-timeline, view-timeline, animation-timeline, and animation-range.

What you'll learn

  • How to create a parallax background effect with a Scroll Timeline in CSS.
  • How to create a progress bar with a Scroll Timeline in CSS.
  • How to create an image reveal effect with a View Timeline in CSS.
  • How to target different types of ranges of a View Timeline in CSS.

What you'll need

One of the following device combinations:

  • A recent version of Chrome (115 or later) on ChromeOS, macOS, or Windows with the "Experimental Web Platform Features" flag set to enabled.
  • A basic understanding of HTML
  • A fundamental understanding of CSS, particularly animations in CSS

2. Get set up

Everything you need for this project is available in a GitHub repository. To get started, clone the code and open it in your favorite dev environment.

  1. Open a new browser tab and navigate to https://github.com/googlechromelabs/io23-scroll-driven-animations-codelab.
  2. Clone the repository.
  3. Open the code in your preferred IDE.
  4. Run npm install to install the dependencies.
  5. Run npm start and visit http://localhost:3000/.
  6. Alternatively, if you don't have npm installed, open the src/index.html file in Chrome.

3. Learn about animation timelines

By default, an animation attached to an element runs on the Document Timeline. That means that when the page loads, the animation ticks forward as time progresses. This is the default animation timeline and, until now, was the only animation timeline you had access to.

With scroll-driven animations, you gain access to two new types of timelines:

  • Scroll Progress Timeline
  • View Progress Timeline

In CSS, these timelines can be attached to an animation using the animation-timeline property. Take a look at what these new timelines mean and how they differ from each other.

Scroll Progress Timeline

A Scroll Progress Timeline is an animation timeline that is linked to progress in the scroll position of a scroll container–also called scrollport or scroller–along a particular axis. It converts a position in a scroll range into a percentage of progress along a timeline.

The starting scroll position represents 0% progress and the ending scroll position represents 100% progress. In the following visualization, note that the progress counts up from 0% to 100% as you scroll down the scroller.

View Progress Timeline

This type of timeline is linked to the relative progress of a particular element within a scroll container. Just like a Scroll Progress Timeline, a scroller's scroll offset is tracked. Unlike a Scroll Progress Timeline, it's the relative position of a subject within that scroller that determines the progress. This is comparable to IntersectionObserver, which tracks how much an element is visible in the scroller. If the element is not visible in the scroller, it is not intersecting. If it is visible inside the scroller–even for the smallest part–it is intersecting.

A View Progress Timeline begins from the moment a subject starts intersecting with the scroller and ends when the subject stops intersecting the scroller. In the following visualization, note that the progress starts counting up from 0% when the subject enters the scroll container and reaches 100% by the time it leaves the scroll container.

By default, an animation linked to the View Progress Timeline attaches to the entire range of it. This starts from the moment the subject enters the scrollport and ends when the subject leaves the scrollport.

It is also possible to link it to a specific part of the View Progress Timeline by specifying the range that it should attach to. This can be, for example, only when the subject is entering the scroller. In the following visualization, the progress starts counting up from 0% when the subject enters the scroll container but already reaches 100% from the moment it is entirely intersecting.

The possible View Timeline ranges that you can target are cover, contain, entry, exit, entry-crossing, and exit-crossing. These ranges are explained later in this codelab but if you can't wait to know, use the tool located at https://goo.gle/view-timeline-range-tool to see what each range represents.

4. Create a parallax background effect

The first effect to add to the page is a parallax background effect on the main background image. As you scroll down the page, the background image should move, although at a different speed. For this, you rely on a Scroll Progress Timeline.

To implement this, there are two steps to take:

  1. Create an animation that moves the background image's position.
  2. Link the animation to the document's scroll progression.

Create the animation

  1. To create the animation, use a regular set of keyframes. In it, move the background position from 0% vertically to 100%:

src/styles.css

@keyframes move-background {
  from {
    background-position: 50% 0%;
  }
  to {
    background-position: 50% 100%;
  }
}
  1. Now attach these keyframes to the body element:

src/styles.css

body {
  animation: 1s linear move-background;
}

With this code, the move-background animation is added to the body element. Its animation-duration property is set to one second, and it uses a linear easing.

The easiest way to create a Scroll Progress timeline is to use the scroll() function. This creates an anonymous Scroll Progress Timeline that you can set as the value for the animation-timeline property.

The scroll() function accepts a <scroller> and an <axis> argument.

Accepted values for the <scroller> argument are the following:

  • nearest. Uses the nearest ancestor scroll container (default).
  • root. Uses the document viewport as the scroll container.
  • self. Uses the element itself as the scroll container.

Accepted values for the <axis> argument are the following:

  • block. Uses the measure of progress along the block axis of the scroll container (default).
  • inline. Uses the measure of progress along the inline axis of the scroll container.
  • y. Uses the measure of progress along the y axis of the scroll container.
  • x. Uses the measure of progress along the x axis of the scroll container.

To link the animation to the root scroller on the block axis, the values to pass into scroll() are root and block. Put together, the value is scroll(root block).

  • Set scroll(root block) as the value for the animation-timeline property on the body.
  • Furthermore, since an animation-duration expressed in seconds makes no sense, set the duration to auto. If you don't specify an animation-duration, it will default to auto.

src/styles.css

body {
  animation: linear move-background;
  animation-duration: auto;
  animation-timeline: scroll(root block);
}

As the root scroller also happens to be the nearest parent scroller for the body element, you can also use a value of nearest:

src/styles.css

body {
  animation: linear move-background;
  animation-duration: auto;
  animation-timeline: scroll(nearest block);
}

Because nearest and block are the default values, you can even choose to omit them. In that case, the code can be simplified to this:

src/styles.css

body {
  animation: linear move-background;
  animation-duration: auto;
  animation-timeline: scroll();
}

Verify your changes

If all went well, you should now have this:

If not, check out the solution-step-1 branch of the code.

5. Create a progress bar for the image gallery

On the page is a horizontal carousel that needs a progress bar to indicate which photo you are currently viewing.

The markup for the carousel looks like this:

src/index.html

<div class="gallery">
  <div class="gallery__scrollcontainer" style="--num-images: 3;">
    <div class="gallery__progress"></div>
    <div class="gallery__entry">
      ...
    </div>
    <div class="gallery__entry">
      ...
    </div>
    <div class="gallery__entry">
      ...
    </div>
  </div>
</div>

The keyframes for the progress bar are already in place and look like this:

src/styles.css

@keyframes adjust-progress {
  from {
    transform: scaleX(calc(1 / var(--num-images)));
  }
  to {
    transform: scaleX(1);
  }
}

This animation needs to be attached to the .gallery__progress element with a Scroll Progress Timeline. As shown in the previous step, you can achieve this by creating an anonymous Scroll Progress Timeline with the scroll() function:

src/styles.css

.gallery__progress {
  animation: linear adjust-progress;
  animation-duration: auto;
  animation-timeline: scroll(nearest inline);
}

While this piece of code might seem like it would work, it does not because of how automatic scroll container lookups using nearest work. When looking up the nearest scroller, the element will only consider the elements that can affect its position. Because .gallery__progress is absolutely positioned, the first parent element that will determine its position is the .gallery element as it has position: relative applied. This means that the .gallery__scrollcontainer element–which is the scroller that needs to be targeted–is not considered at all during this automatic lookup.

To work around this, create a named Scroll Progress Timeline on the .gallery__scrollcontainer element and link the .gallery__progress to it using that name.

To create a named Scroll Progress Timeline on an element, set the scroll-timeline-name CSS property on the scroll container to a value of your liking. The value must start with --.

Because the gallery scrolls horizontally, you also need to set the scroll-timeline-axis property. Allowed values are the same as the <axis> argument of scroll().

Finally, to link the animation to the Scroll Progress Timeline, set the animation-timeline property on the element that needs to be animated to the same value as the identifier used for the scroll-timeline-name.

  • Change the styles.css file to include the following:

src/styles.css

.gallery__scrollcontainer {
  /* Create the gallery-is-scrolling timeline */
  scroll-timeline-name: --gallery-is-scrolling;
  scroll-timeline-axis: inline;
}

.gallery__progress {
  animation: linear adjust-progress;
  animation-duration: auto;
  /* Set gallery-is-scrolling as the timeline */
  animation-timeline: --gallery-is-scrolling;
}

Verify your changes

If all went well, you should now have this:

If not, check out the solution-step-2 branch of the code.

6. Animate the gallery images as they enter and exit the scrollport

Set up an anonymous View Progress Timeline

A nice effect to add is to fade in the gallery images as they come into view. For this, you can use a View Progress Timeline.

To create a View Progress Timeline, you can use the view() function. Its accepted arguments are <axis> and <view-timeline-inset>.

  • The <axis> is the same as from the Scroll Progress Timeline and defines which axis to track.
  • With <view-timeline-inset>, you can specify an offset (positive or negative) to adjust the bounds when an element is considered to be in view or not.
  • The keyframes are already in place, so you only need to attach them. To do so, create a View Progress Timeline on each .gallery__entry element.

src/styles.css

@keyframes animate-in {
  from {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
  to {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
}

.gallery__entry {
  animation: linear animate-in;
  animation-duration: auto;
  animation-timeline: view(inline);
}

Limit the range of a View Progress Timeline

If you save the CSS and load the page, you see the elements fade in, but something seems off. They start at opacity 0 when they are entirely out of view and only end at opacity 1 when they have fully exited.

That is because the default range for a View Progress Timeline is the full range. This is known as the cover range.

  1. To target only the entry range of the subject, use the animation-range CSS property in order to limit when the animation should run.

src/styles.css

.gallery__entry {
  animation: linear fade-in;
  animation-duration: auto;
  animation-timeline: view(inline);
  animation-range: entry 0% entry 100%;
}

The animation now runs from entry 0% (the subject is about to enter the scroller) to entry 100% (the subject has fully entered the scroller).

The possible View Timeline Ranges are the following:

  • cover. Represents the full range of the view progress timeline.
  • entry. Represents the range during which the principal box is entering the view progress visibility range.
  • exit. Represents the range during which the principal box is exiting the view progress visibility range.
  • entry-crossing. Represents the range during which the principal box crosses the end border edge.
  • exit-crossing. Represents the range during which the principal box crosses the start border edge.
  • contain. Represents the range during which the principal box is either fully contained by, or fully covers, its view progress visibility range within the scrollport. This depends on whether the subject is taller or shorter than the scroller.

Use the tool located at https://goo.gle/view-timeline-range-tool to see what each range represents and how the percentages affect the start and end positions.

  1. Because the start and end ranges are the same here and the default offsets are used, simplify the animation-range to a single animation range name:

src/styles.css

.gallery__entry {
  animation: linear animate-in;
  animation-duration: auto;
  animation-timeline: view(inline);
  animation-range: entry;
}
  • To fade out the images as they exit the scroller, you can do the same as the animate-in animation but target a different range.

src/styles.css

@keyframes animate-out {
  from {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
  to {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
}

.gallery__entry {
  animation: linear animate-in, linear animate-out;
  animation-duration: auto;
  animation-timeline: view(inline);
  animation-range: entry, exit;
}

The animate-in keyframes will be applied to the entry range and the animate-out keyframes to the exit range.

Verify your changes

If all went well, you should now have this:

If not, check out the solution-step-3 branch of the code.

7. Animate the gallery images as they enter and exit the scrollport, using one set of keyframes

The case for one set of keyframes

Instead of attaching two animations to different ranges, it is possible to create one set of keyframes that already contains the range information.

The shape of the keyframes looks like this:

@keyframes keyframes-name {
  range-name range-offset {
    ...
  }
  range-name range-offset {
    ...
  }
}
  1. Combine the fade-in and fade-out keyframes like this:

src/styles.css

@keyframes animate-in-and-out {
  entry 0% {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
  entry 90% {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }

  exit 10% {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
  exit 100% {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
}
  1. When the range information is present in the keyframes, you don't need to specify the animation-range separately anymore. Attach the keyframes as the animation property.

src/styles.css

.gallery__entry {
  animation: linear animate-in-and-out both;
  animation-duration: auto;
  animation-timeline: view(inline);
}

Verify your changes

If all went well, you should have the same result as from the previous step. If not, check out the solution-step-4 branch of the code.

8. Congratulations!

You finished this codelab and now know how to create Scroll Progress Timelines and View Progress Timelines in CSS!

Learn More

Resources: