Add entry and exit effects to elements as they enter or exit the scrollport

Web features: Scroll-driven animations

Entry and exit effects are animations that are triggered when an element enters or leaves the viewport. This can be used to create engaging and dynamic user experiences. For example, you can use an entry effect to fade in an element as it scrolls into view, or an exit effect to scale it down as it scrolls out of view.

How to implement

To add entry and exit effects to an element, you need to combine a few CSS properties. Here’s a step-by-step guide:

  1. Create separate @keyframes for the entry and exit animations. The entry animation will be applied as the element enters the viewport, and the exit animation will be applied as it leaves.

    @keyframes slide-in {
      from { transform: translateX(-100%); }
    }
    @keyframes slide-out {
      to { transform: translateX(100%); }
    }
    
  2. Attach the entry and exit keyframes to the element. You can do this by defining multiple animations in the animation property.

    • Give the entry animation an animation-fill-mode of backwards so that it applies its initial state before the animation starts.
    • Give the exit animation an animation-fill-mode of forwards so that it maintains its final state after the animation is complete.
    .animated-element {
      animation:
        slide-in 1s linear backwards,
        slide-out 1s linear forwards;
    }
    
  3. Create a View Timeline and link it to the animations. A View Timeline is a type of timeline that is linked to the visibility of an element in the viewport. You can create one using the view() function and then apply it to your animations using the animation-timeline property.

    .animated-element {
      animation-timeline: view();
    }
    

    By default, view() tracks the element on the block axis. If you need to track it on the inline axis, you can use view(inline).

  4. Limit the animations to the entry and exit ranges. The animation-range property allows you to specify which part of the timeline an animation should run on.

    • The entry range covers the time from when the element first enters the viewport until it is fully visible.
    • The exit range covers the time from when the element starts to leave the viewport until it is completely hidden.
    .animated-element {
      animation-range: entry, exit;
    }
    

Example code

This code animates the direct children of the scroller on scroll using an anonymous view-timeline:

@media (prefers-reduced-motion: no-preference) {
  @supports ((animation-timeline: view()) and (animation-range: entry)) {
    @keyframes grow {
      from {
        scale: 0.5;
      }
    }
    @keyframes shrink {
      to {
        scale: 0.5;
      }
    }

    .scroller > * {
      animation:
        grow auto linear backwards,
        shrink auto linear forwards;
      animation-timeline: view(inline);
      animation-range: entry, exit;
    }
  }
}

As the elements enter the scrollport the grow animation is played, and as they leave the scrollport the shrink animation is played.

The following code has the same visual outcome, but animates the direct children of the scroller on scroll using an named view-timeline:

@media (prefers-reduced-motion: no-preference) {
  @supports ((animation-timeline: view()) and (animation-range: entry)) {
    @keyframes grow {
      from {
        scale: 0.5;
      }
    }
    @keyframes shrink {
      to {
        scale: 0.5;
      }
    }

    .scroller > * {
      view-timeline: --tl inline;
      animation:
        grow auto linear backwards,
        shrink auto linear forwards;
      animation-timeline: --tl;
      animation-range: entry, exit;
    }
  }
}

Best Practices

When using scroll-driven animations, it's important to follow a few best practices to ensure a smooth and accessible experience:

  • DO include feature detection: Not all browsers support scroll-driven animations. Use @supports ((animation-timeline: view()) and (animation-range: entry)) to check for support and provide a fallback for browsers that don't support it.
    • The (animation-range: entry) check MUST be included here, to filter out browsers with only partial support.
    • DO NOT use the scroll-timeline-polyfill package for the fallback strategy as it is not feature complete and has a lot of known issues.
    • If the animation is only considered to be decorative, opt for Progressive Enhancement and DO NOT provide a fallback.
  • DO respect user preferences: Some users prefer to have less motion on the web. Use the prefers-reduced-motion media query to disable or reduce your animations for these users.
  • DO try to animate only performant CSS properties: For the smoothest animations, stick to animating properties that can be handled by the browser's compositor thread, such as transform and opacity. Animating other properties like width or height can lead to performance issues.
  • DO use the correct declaration order: When using the animation shorthand property, declare animation-timeline after it to prevent the shorthand from resetting the timeline.

When using the view() function to create a scroll-driven animation:

  • OPTIONAL be explicit about the axis to track: When not targeting the default block axis (such as in a horizontal scroller), be explicit about which axis to track with view(block) or view(inline).
  • When the animation is not applied to the tracked subject itself, use a named view timeline.

When using the view-timeline property to create a scroll-driven animation:

  • DO use a CSS <dashed-ident> for the name.
  • OPTIONAL be explicit about the axis to track: When not targeting the default block axis (such as in a horizontal scroller), be explicit about which axis to track with view-timeline-axis.
  • DO make sure the scope of the lookup works: When the element that is declaring the view-timeline is not a flat tree ancestor of the animated element, hoist up the visibility of the view-timeline’s name by using timeline-scope on a shared ancestor.

Prefer a named view-timeline when multiple elements or children of the tracked subject need to animate.

Browser support and fallback strategies

Therefore, a fallback strategy is typically required.

For browsers that do not support scroll-driven animations, you can use a fallback to recreate the visual effects. The fallbacks are typically built with either a scroll listener (for ScrollTimeline effects) or the IntersectionObserver API (for ViewTimeline effects).

In browsers with built-in support for scroll-driven animations, ALWAYS use the native CSS implementation as those are more performant.

Note that not every effect can be recreated using the fallbacks approach.

For this use-case specifically, the following script applies the fallback for browsers that do not support scroll-driven animations. It uses an IntersectionObserver to track the visibility of the .wrapper element and updates the transform property of the layers based on the scroll position.

<script>
  if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          // This matches the effect as defined in the CSS example above.
          // Customize this further if needed.
          entry.target.style.scale = 0.5 + entry.intersectionRatio * 0.5;
        }
      },
      {
        threshold: Array.from({ length: 101 }, (_, i) => i / 100),
      }
    );

    document.querySelectorAll('.scroller > *').forEach((el) => {
      observer.observe(el);
    });
  }
</script>