为元素添加进入和退出滚动视口时的效果

Web 功能: 滚动条驱动的动画

进入和退出效果是指在元素进入或离开视口时触发的动画。这可用于打造富有吸引力的动态用户体验。例如,您可以使用进入效果让元素在滚动到视图中时淡入,或使用退出效果让元素在滚动出视图时缩小。

如何实现

如需为元素添加进入和退出效果,您需要组合使用一些 CSS 属性。以下是详细的分步指南:

  1. 为进入动画和退出动画创建单独的 @keyframes。进入动画将在元素进入视口时应用,退出动画将在元素离开视口时应用。

    @keyframes slide-in {
      from { transform: translateX(-100%); }
    }
    @keyframes slide-out {
      to { transform: translateX(100%); }
    }
    
  2. 将进入和退出关键帧附加到元素。为此,您可以在 animation 属性中定义多个动画。

    • 为进入动画指定 backwards 的 animation-fill-mode,以便在动画开始前应用其初始状态。
    • 为退出动画指定 forwards 的 animation-fill-mode,以便在动画完成后保持其最终状态。
    .animated-element {
      animation:
        slide-in 1s linear backwards,
        slide-out 1s linear forwards;
    }
    
  3. 创建查看时间轴并将其关联到动画。视图时间轴是一种与元素在视口中的可见性相关联的时间轴。您可以使用 view() 函数创建一个,然后使用 animation-timeline 属性将其应用于动画。

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

    默认情况下,view() 会跟踪 block 轴上的元素。如果您需要在 inline 轴上跟踪它,可以使用 view(inline)。

  4. 将动画限制在 entry 和 exit 范围内。借助 animation-range 属性,您可以指定动画应在时间轴的哪个部分运行。

    • entry 范围涵盖从元素首次进入视口到完全可见的时间。
    • exit 范围涵盖从元素开始离开视口到完全隐藏的时间。
    .animated-element {
      animation-range: entry, exit;
    }
    

示例代码

此代码使用匿名视图时间轴在滚动时为滚动条的直接子级添加动画效果:

@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;
    }
  }
}

当元素进入滚动端口时,系统会播放 grow 动画;当元素离开滚动端口时,系统会播放 shrink 动画。

以下代码具有相同的视觉效果,但会使用命名的 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;
    }
  }
}

最佳做法

使用滚动驱动动画时,请务必遵循一些最佳实践,以确保提供流畅且无障碍的体验:

  • 务必添加功能检测:并非所有浏览器都支持滚动驱动的动画。使用 @supports ((animation-timeline: view()) and (animation-range: entry)) 检查是否支持,并为不支持的浏览器提供后备方案。
    • 此处必须包含 (animation-range: entry) 检查,以滤除仅提供部分支持的浏览器。
    • 请勿将 scroll-timeline-polyfill 软件包用于回退策略,因为它功能不完整,并且存在许多已知问题。
    • 如果动画仅被视为装饰性元素,请选择渐进增强,不要提供后备。
  • 务必尊重用户偏好:有些用户更喜欢在网页上看到较少的动画。使用 prefers-reduced-motion 媒体查询为这些用户停用或减少动画。
  • 务必尝试仅为高性能 CSS 属性添加动画效果:为了获得最流畅的动画效果,请坚持为可由浏览器的合成器线程处理的属性(例如 transform 和 opacity)添加动画效果。为其他属性(例如 width 或 height)添加动画效果可能会导致性能问题。
  • 务必使用正确的声明顺序:使用 animation 简写属性时,请在其后声明 animation-timeline,以防止简写属性重置时间轴。

使用 view() 函数创建滚动驱动的动画时:

  • 可选:明确指定要跟踪的轴。如果未定位到默认的 block 轴(例如在水平滚动条中),请使用 view(block) 或 view(inline) 明确指定要跟踪的轴。
  • 如果动画未应用于被跟踪的主体本身,请使用命名的视图时间轴。

使用 view-timeline 属性创建滚动驱动的动画时:

  • 务必使用 CSS <dashed-ident> 作为名称。
  • 可选:明确指定要跟踪的轴。如果目标不是默认的 block 轴(例如在水平滚动条中),请使用 view-timeline-axis 明确指定要跟踪的轴。
  • 务必确保查找范围有效:如果声明 view-timeline 的元素不是动画元素的平面树祖先,请在共享祖先上使用 timeline-scope 来提升 view-timeline 名称的可见性。

当被跟踪主体的多个元素或子级需要添加动画效果时,最好使用命名的 view-timeline。

浏览器支持和后备策略

因此,通常需要采用后备策略。

对于不支持滚动驱动动画的浏览器,您可以使用后备方案来重现视觉效果。后备方案通常使用滚动监听器(用于 ScrollTimeline 效果)或 IntersectionObserver API(用于 ViewTimeline 效果)构建。

在内置支持滚动驱动动画的浏览器中,请始终使用原生 CSS 实现,因为这些实现的性能更高。

请注意,并非所有效果都可以通过后备方法重现。

具体而言,对于此用例,以下脚本会为不支持滚动驱动动画的浏览器应用回退。它使用 IntersectionObserver 跟踪 .wrapper 元素的可见性,并根据滚动位置更新图层的 transform 属性。

<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>