Blog

Using scroll-driven animations for better transitions

The new CSS scroll-driven animations API allows for more intricate and interactive transitions.

CSS scroll-driven animations are a new way of declaring animations that are not chronological. Instead of declaring a specific duration, you use a function that maps a position to a specific frame in the animation. This makes it easier to add scroll-driven animations such as the parallax effect. But it can also make other transitions easier to apply, even when the direct relation between animation position and user interaction is not required.

Transitions vs Animations

When working with interactive elements, we often think of them as having different states, and animations tend to be transitions from one state to another. Not surprisingly, these are commonly made by adding the CSS transition property on the default state. Contrast this with actual animations, defined with @keyframes, where there is a default state and an animation from one to several defined states over a certain duration. With the new properties the timeline is a function of user interaction. The timeline is in essence mapped onto the interaction: for scroll(), the first frame would correspond to the start scroll position and the last frame would correspond to the final scroll position. For view(), they are mapped on the visibility of the element, where the first keyframe is the start of the visibility of the element, and the last keyframe is when the the element's opposite edge moves out of the containing scrolling element.

Example: A scrollable slideshow

Consider a horizontal list of images where all the images except for the image that is in view are grayscale. Without the scroll-driven animations, you could say that slides have either an active or an inactive state, the former being full-color, and the latter being grayscale. When a slide is fully visible, its state should be active, otherwise, it should be inactive. From here, the straightforward solution would be to create two classes, and add an IntersectionObserver with a callback that switches classes the moment a slide enters or exits the frame. Try scrolling horizontally in the following example:

Sorry, your browser does not support the technology in this demo

Now consider the same behavior with scroll-driven animation with animation-timeline: view(). Instead of two states, you could view a slide as having an animation, starting when the slide enters the viewport and ending when the slide exits the frame. This means that the middle of the animation would be the moment where the slide is in the center of the frame, the implicit active state.

This becomes clearer if we create two distinct animations with a shared duration, one named move and one named highlight. We can use the former to mimic the scroll behavior, and have the latter do the transition:

Sorry, your browser does not support the technology in this demo

.example__slide {
  animation-name:  move, highlight;
  animation-duration: 5s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
  animation-diretion: alternate;
}

@keyframes move {
  0% {
    transform: translateX(130%);
  }
  100% {
    transform: translateX(-110%);
  }
}

@keyframes highlight {
  0% {
    background-color: transparent;
  }
  50% {
    background-color: blue;
  }
  100% {
    background-color: transparent;
  }
}

Now instead of viewing this animation as a function of time, consider it as a function of the scroll position of the container, and consequently a function of the position of the slide. This means we can remove the move animation, and specify the animation-timeline as a function of the view. If you're in a Chromium-based browser, you can horizontally scroll the following example to see it in action:

Sorry, your browser does not support the technology in this demo

.example__slide {
  animation-timeline: view(x);
  animation-name: highlight;
}

Further tweaks can be made by specifying different offsets for where the animation-timeline should start or end, so that the apex of falls on the resting point of the slide. In the following example the offsets for all slides make sure the triggering viewbox has an inset, so that partially visible slides will only start the animation when they cross the inset boundary. An exception for the first child is needed to shift the left side of the viewbox to align it to the visible side of the container.

Sorry, your browser does not support the technology in this demo

.example__slide:first-child {
  animation-timeline: view(x 0 var(--offset));
}  
.example__slide:last-child {
  animation-timeline: view(x var(--offset) 0);
}

Even though this behavior might not need to be tied to scroll interaction, doing so results in a more succinct implementation, allows for more intricate, multi-step animations as transitions. And not to forget, with the added benefit of working without Javascript! For now, this is only supported in Chromium-based browsers, but support for Firefox and Safari is coming.

← All blog posts