使用滚动驱动的动画,在滚动时为元素添加动画效果

了解如何使用滚动时间轴和视图时间轴以声明方式创建滚动驱动型动画。

发布日期:2023 年 5 月 5 日

滚动条驱动的动画

Browser Support

  • Chrome: 115.
  • Edge: 115.
  • Firefox: behind a flag.
  • Safari: not supported.

Source

滚动条驱动的动画是 Web 上常见的用户体验模式。滚动条驱动的动画与滚动容器的滚动位置相关联。这意味着,当您向上或向下滚动时,关联的动画会直接向前或向后快进/快退。例如,视差背景图片或随着滚动而移动的阅读指示器等效果。

文档顶部的阅读指示器,由滚动驱动。

与滚动条驱动的动画类似的动画是指与元素在其滚动容器中的位置相关联的动画。例如,借助它,元素可以在进入视野时淡入。

此页面上的图片会在进入视野时逐渐淡入。

实现此类效果的传统方法是在主线程上响应滚动事件,这会导致两个主要问题:

  • 现代浏览器在单独的进程中执行滚动,因此会异步传送滚动事件。
  • 主线程动画可能会出现卡顿

因此,创建与滚动同步且性能出色的滚动条驱动型动画是不可能的,或者非常困难。

从 Chrome 115 版开始,您可以使用一组新的 API 和概念来启用声明式滚动驱动型动画:滚动时间轴和视图时间轴。

这些新概念可与现有的 Web Animations API (WAAPI)CSS Animations API 集成,从而继承这些现有 API 带来的优势。这包括能够在主线程之外运行滚动条驱动的动画。没错,您没看错:现在,只需额外编写几行代码,即可在主线程上运行由滚动驱动的流畅动画。这有什么不好?

网页上的动画,简要回顾

使用 CSS 在 Web 上制作动画

如需在 CSS 中创建动画,请使用 @keyframes at 规则定义一组关键帧。使用 animation-name 属性将其关联到元素,同时设置 animation-duration 以确定动画应持续多长时间。还有更多 animation-* 长写法属性可供使用,例如 animation-easing-functionanimation-fill-mode,这些属性都可以在 animation 简写法中组合使用。

例如,下面的动画会在沿 X 轴放大元素的同时更改其背景颜色:

@keyframes scale-up {
  from {
    background-color: red;
    transform: scaleX(0);
  }
  to {
    background-color: darkred;
    transform: scaleX(1);
  }
}

#progressbar {
  animation: 2.5s linear forwards scale-up;
}

使用 JavaScript 在 Web 上创建动画

在 JavaScript 中,可以使用 Web Animations API 来实现完全相同的效果。为此,您可以创建新的 AnimationKeyFrameEffect 实例,也可以使用更短的 Element animate() 方法

document.querySelector('#progressbar').animate(
  {
    backgroundColor: ['red', 'darkred'],
    transform: ['scaleX(0)', 'scaleX(1)'],
  },
  {
    duration: 2500,
    fill: 'forwards',
    easing: 'linear',
   }
);

上述 JavaScript 代码段的此视觉结果与之前的 CSS 版本完全相同。

动画时间轴

默认情况下,附加到元素的动画会在文档时间轴上运行。其原始时间从网页加载时开始的 0 开始,并随着时钟时间的推移而向前跳动。这是默认的动画时间轴,到目前为止,您只能使用此动画时间轴。

滚动驱动型动画规范定义了两种可供您使用的新时间轴:

  • 滚动进度时间轴:与滚动容器沿特定轴的滚动位置相关联的时间轴。
  • 查看进度时间轴:与特定元素在其滚动容器中的相对位置相关联的时间轴。

滚动进度时间轴

滚动进度时间轴是与滚动容器(也称为滚动端口滚动条)沿特定轴的滚动位置相关联的动画时间轴。它会将滚动范围中的位置转换为进度百分比。

起始滚动位置表示 0% 的进度,结束滚动位置表示 100% 的进度。在下面的可视化图表中,您可以看到,当您从上到下滚动滚动条时,进度会从 0% 计数到 100%。

滚动进度时间轴的可视化图表。当您向下滚动到滚动条底部时,进度值会从 0% 计数到 100%。

✨ 亲自试用

滚动进度时间轴通常简称为“滚动时间轴”。

查看进度时间轴

此类时间轴与滚动容器中特定元素的相对进度相关联。与滚动进度时间轴一样,系统会跟踪滚动条的滚动偏移量。与滚动进度时间轴不同,决定进度的是该滚动条中主题的相对位置。

这与 IntersectionObserver 的工作原理有点类似,后者可以跟踪元素在滚动条中的可见程度。如果该元素在滚动条中不可见,则不会交叉。如果它在滚动条内可见(即使是最小部分),则会交叉。

查看进度时间轴从主题开始与滚动条相交开始,到主题停止与滚动条交叉时结束。在下面的可视化图表中,您可以看到,当主题进入滚动容器时,进度会从 0% 开始计数;当主题离开滚动容器时,进度会达到 100%。

查看进度时间轴的可视化结果。随着主题(绿色框)穿过滚动条,进度会从 0% 计数到 100%。

✨ 亲自试用

查看进度时间轴通常简称为“查看时间轴”。您可以根据正文的大小定位查看时间轴的特定部分,但我们稍后会详细介绍。

滚动进度时间轴的实用指南

在 CSS 中创建匿名滚动进度时间轴

在 CSS 中创建滚动时间轴的最简单方法是使用 scroll() 函数。此函数将创建一个匿名的滚动时间轴,您可以将其设为新的 animation-timeline 属性的值。

示例:

@keyframes animate-it {  }

.subject {
  animation: animate-it linear;
  animation-timeline: scroll(root block);
}

scroll() 函数接受 <scroller><axis> 参数。

<scroller> 参数接受的值如下:

  • nearest:使用最近的父级滚动容器(默认)。
  • root:使用文档视口作为滚动容器。
  • self:使用元素本身作为滚动容器。

<axis> 参数接受的值如下:

  • block:使用滚动容器的块轴进度测量(默认)
  • inline:使用滚动容器的内轴进度测量。
  • y:使用滚动容器的 y 轴进度测量。
  • x:使用滚动容器的 x 轴进度测量。

例如,如需将动画绑定到块轴上的根滚动条,则传入 scroll() 的值为 rootblock。合起来,值为 scroll(root block)

演示:阅读进度指示器

此演示中的阅读进度指示器固定在视口顶部。当您向下滚动页面时,进度条会不断变长,直到到达文档末尾时占据整个视口宽度。一个匿名的滚动进度时间轴用于驱动动画。

演示:阅读进度指示器

✨ 亲自试用

阅读进度指示器使用固定位置放置在页面顶部。如需利用复合动画,请使用 transform 在 x 轴上缩小元素,而不是对 width 应用动画。

<body>
  <div id="progress"></div>
  …
</body>
@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

#progress {
  position: fixed;
  left: 0; top: 0;
  width: 100%; height: 1em;
  background: red;

  transform-origin: 0 50%;
  animation: grow-progress auto linear;
  animation-timeline: scroll();
}

#progress 元素上的动画 grow-progress 的时间轴设置为使用 scroll() 创建的匿名时间轴。未向 scroll() 提供任何参数,因此它将回退到默认值。

要跟踪的默认滚动条是 nearest 滚动条,默认轴是 block。这会有效地定位到根滚动条,因为它是 #progress 元素最近的滚动条,同时跟踪其块方向。

在 CSS 中创建命名的滚动进度时间轴

定义滚动进度时间轴的另一种方法是使用命名的时间轴。虽然这种方法有点繁琐,但当您没有定位父滚动条或根滚动条或者网页使用多个时间轴或者自动查找不起作用时,这种方法可能会很有用。这样,您就可以通过为其指定的名称来识别滚动进度时间轴。

如需在元素上创建命名的滚动进度时间轴,请将滚动容器上的 scroll-timeline-name CSS 属性设为您喜欢的标识符。该值必须以 -- 开头。

如需调整要跟踪的轴,还需要声明 scroll-timeline-axis 属性。允许的值与 scroll()<axis> 实参相同。

最后,若要将动画关联到滚动进度时间轴,请将需要添加动画效果的元素上的 animation-timeline 属性设为与 scroll-timeline-name 所用的标识符相同的值。

代码示例:

@keyframes animate-it {  }

.scroller {
  scroll-timeline-name: --my-scroller;
  scroll-timeline-axis: inline;
}

.scroller .subject {
  animation: animate-it linear;
  animation-timeline: --my-scroller;
}

如果需要,您可以在 scroll-timeline 简写形式中组合使用 scroll-timeline-namescroll-timeline-axis。例如:

scroll-timeline: --my-scroller inline;

此演示中,每个图片轮播界面上方都会显示一个步骤指示器。如果轮播界面包含三张图片,指示器栏的宽度从 33% 开始,表示您当前正在查看三张图片中的第 1 张。当最后一张图片显示在视野中时(由滚动条滚动到底部决定),指示器会占据滚动条的整个宽度。命名的滚动进度时间轴用于驱动动画。

演示:横向轮播界面步骤指示器

✨ 亲自试用

图库的基本标记如下所示:

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

.gallery__progress 元素在 .gallery 封装容器元素内采用绝对定位。其初始大小由 --num-images 自定义属性决定。

.gallery {
  position: relative;
}


.gallery__progress {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 1em;
  transform: scaleX(calc(1 / var(--num-images)));
}

.gallery__scrollcontainer 会水平布局包含的 .gallery__entry 元素,并且是滚动元素。通过跟踪其滚动位置,.gallery__progress 会获得动画效果。为此,您可以引用名为 --gallery__scrollcontainer 的滚动进度时间轴。

@keyframes grow-progress {
  to { transform: scaleX(1); }
}

.gallery__scrollcontainer {
  overflow-x: scroll;
  scroll-timeline: --gallery__scrollcontainer inline;
}
.gallery__progress {
  animation: auto grow-progress linear forwards;
  animation-timeline: --gallery__scrollcontainer;
}

使用 JavaScript 创建滚动进度时间轴

如需在 JavaScript 中创建滚动时间轴,请创建 ScrollTimeline 类的新实例。传入包含要跟踪的 sourceaxis 的属性值包。

  • source:对您要跟踪其滚动条的元素的引用。使用 document.documentElement 定位到根滚动条。
  • axis:确定要跟踪的轴。与 CSS 变体类似,可接受的值包括 blockinlinexy
const tl = new ScrollTimeline({
  source: document.documentElement,
});

如需将其附加到 Web 动画,请将其作为 timeline 属性传入,并忽略所有 duration(如果有)。

$el.animate({
  opacity: [0, 1],
}, {
  timeline: tl,
});

演示:阅读进度指示器(再访)

如需使用 JavaScript 重新创建阅读进度指示器,同时使用相同的标记,请使用以下 JavaScript 代码:

const $progressbar = document.querySelector('#progress');

$progressbar.style.transformOrigin = '0% 50%';
$progressbar.animate(
  {
    transform: ['scaleX(0)', 'scaleX(1)'],
  },
  {
    fill: 'forwards',
    timeline: new ScrollTimeline({
      source: document.documentElement,
    }),
  }
);

CSS 版本中的视觉效果完全相同:创建的 timeline 会跟踪根滚动条,并在您滚动页面时将 x 轴上的 #progress 从 0% 放大到 100%。

✨ 亲自试用

实用指南:使用观看进度时间轴

在 CSS 中创建匿名查看进度时间轴

如需创建查看进度时间轴,请使用 view() 函数。它接受的参数为 <axis><view-timeline-inset>

  • <axis> 与滚动进度时间轴中的 <axis> 相同,用于定义要跟踪的轴。默认值为 block
  • 借助 <view-timeline-inset>,您可以指定偏移量(正值或负值)来调整元素是否在视野范围内的边界。该值必须为百分比或 auto,其中 auto 为默认值。

例如,如需将动画绑定到与其滚动条在块轴上相交的元素,请使用 view(block)。与 scroll() 类似,将其设置为 animation-timeline 属性的值,同时别忘了将 animation-duration 设置为 auto

使用以下代码,每个 img 都会在您滚动时越过视口时逐渐淡入。

@keyframes reveal {
  from { opacity: 0; }
  to { opacity: 1; }
}

img {
  animation: reveal linear;
  animation-timeline: view();
}

插播:查看时间轴范围

默认情况下,与视图时间轴关联的动画会附加到整个时间轴范围。此动画从主题即将进入滚动端口的那一刻开始,到主题完全离开滚动端口时结束。

您还可以通过指定其应附加到的范围,将其关联到视图时间轴的特定部分。例如,这可以是仅当主题进入滚动条时的情况。在下面的可视化图表中,当主题进入滚动容器时,进度会从 0% 开始计数,但从完全交叉的那一刻起,进度便达到 100%。

设置为跟踪主题条目范围的视图时间轴。动画仅在主题进入滚动端口时运行。

您可以定位到的可能的查看时间轴范围如下:

  • cover:表示查看进度时间轴的完整范围。
  • entry:表示主框进入查看进度可见性范围的范围。
  • exit:表示主框退出查看进度可见性范围的范围。
  • entry-crossing:表示主框跨越结束边框边缘的范围。
  • exit-crossing:表示主框跨越起始边框边缘的范围。
  • contain:表示主框完全位于或完全覆盖滚动端口内查看进度可见性范围的范围。这取决于主题比滚动条更高还是更短。

如需定义范围,您必须设置 range-start 和 range-end。每个 range-name 都由 range-name(请参阅上方列表)和 range-offset 组成,用于确定 range-name 中的相应位置。range-offset 通常是介于 0%100% 之间的百分比,但您也可以指定固定长度,例如 20em

例如,如果您想从正文进入画面的那一刻开始运行动画,请选择 entry 0% 作为范围起点。如需在主题进入时完成,请选择 entry 100% 作为范围结束值。

在 CSS 中,您可以使用 animation-range 属性进行设置。示例:

animation-range: entry 0% entry 100%;

在 JavaScript 中,使用 rangeStartrangeEnd 属性。

$el.animate(
  keyframes,
  {
    timeline: tl,
    rangeStart: 'entry 0%',
    rangeEnd: 'entry 100%',
  }
);

使用下方嵌入的工具,了解每个范围名称的含义,以及百分比对起始位置和结束位置的影响。尝试将 range-start 设置为 entry 0% 并将 range-end 设置为 cover 50%,然后拖动滚动条以查看动画效果。

“查看时间轴范围可视化工具”,网址为 https://goo.gle/view-timeline-range-tool

观看录音

在使用“查看时间轴范围”工具时,您可能已经注意到,某些范围可以通过两个不同的范围名称 + 范围偏移组合进行定位。例如,entry 0%entry-crossing 0%cover 0% 都定位到同一区域。

当 range-start 和 range-end 定位到相同的 range-name 且涵盖整个范围(从 0% 到 100%)时,您可以将值缩短为仅包含 range-name。例如,animation-range: entry 0% entry 100%; 可以重写为更短的 animation-range: entry

演示:图片揭示

此演示会在图片进入滚动边界时逐渐淡入。这通过使用匿名视图时间轴来实现。动画范围已调整,以便每张图片在滚动条的中间位置时都处于完全不透明状态。

演示:图片显示

✨ 亲自试用

此展开效果是通过使用带动画的 clip-path 实现的。用于此效果的 CSS 如下所示:

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

.revealing-image {
  animation: auto linear reveal both;
  animation-timeline: view();
  animation-range: entry 25% cover 50%;
}

在 CSS 中创建命名的查看进度时间轴

与滚动时间轴具有命名版本的方式类似,您还可以创建命名的查看时间轴。您应使用带有 view-timeline- 前缀的变体(即 view-timeline-nameview-timeline-axis),而不是 scroll-timeline-* 属性。

适用相同类型的值,并且查找命名时间轴的规则也相同。

演示:再次揭晓图片

重新修改前面的图片展开演示,修改后的代码如下所示:

.revealing-image {
  view-timeline-name: --revealing-image;
  view-timeline-axis: block;

  animation: auto linear reveal both;
  animation-timeline: --revealing-image;
  animation-range: entry 25% cover 50%;
}

使用 view-timeline-name: revealing-image 时,系统会在最近的滚动条中跟踪元素。然后,系统会将相同的值用作 animation-timeline 属性的值。视觉输出与之前完全相同。

✨ 亲自试用

在 JavaScript 中创建查看进度时间轴

如需在 JavaScript 中创建 View 时间轴,请创建 ViewTimeline 类的新实例。传入包含要跟踪的 subjectaxisinset 的属性值包。

  • subject:对您要在其自己的滚动条中跟踪的元素的引用。
  • axis:要跟踪的轴。与 CSS 变体类似,可接受的值包括 blockinlinexy
  • inset:确定框是否在视野范围内时,对滚动端口进行的内嵌(正值)或外嵌(负值)调整。
const tl = new ViewTimeline({
  subject: document.getElementById('subject'),
});

如需将其附加到 Web 动画,请将其作为 timeline 属性传入,并忽略所有 duration(如果有)。(可选)使用 rangeStartrangeEnd 属性传入范围信息。

$el.animate({
  opacity: [0, 1],
}, {
  timeline: tl,
  rangeStart: 'entry 25%',
  rangeEnd: 'cover 50%',
});

✨ 亲自试用

更多可尝试的操作

使用一组关键帧附加到多个 View 时间轴范围

我们来看看这个列表条目带有动画效果的联系人列表演示。当列表条目从底部进入滚动端口时,会滑入并淡入;当列表条目从顶部离开滚动端口时,会滑出并淡出。

演示:联系人名单

✨ 亲自试用

在此演示中,每个元素都会装饰一个 View 时间轴,该时间轴会跟踪元素穿过其滚动边界的情况,但有两个滚动驱动型动画附加到该元素。animate-in 动画会附加到时间轴的 entry 范围,animate-out 动画会附加到时间轴的 exit 范围。

@keyframes animate-in {
  0% { opacity: 0; transform: translateY(100%); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes animate-out {
  0% { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-100%); }
}

#list-view li {
  animation: animate-in linear forwards,
             animate-out linear forwards;
  animation-timeline: view();
  animation-range: entry, exit;
}

您还可以创建一组已包含范围信息的关键帧,而不是运行附加到两个不同范围的两个不同动画。

@keyframes animate-in-and-out {
  entry 0%  {
    opacity: 0; transform: translateY(100%);
  }
  entry 100%  {
    opacity: 1; transform: translateY(0);
  }
  exit 0% {
    opacity: 1; transform: translateY(0);
  }
  exit 100% {
    opacity: 0; transform: translateY(-100%);
  }
}

#list-view li {
  animation: linear animate-in-and-out;
  animation-timeline: view();
}

由于关键帧包含范围信息,因此您无需指定 animation-range。结果与之前完全一样。

✨ 亲自试用

附加到非祖先滚动时间轴

命名滚动时间轴和命名查看时间轴的查找机制仅限于滚动祖先。不过,很多情况下,需要添加动画效果的元素并非需要跟踪的滚动条的子元素。

为了实现这一点,需要使用 timeline-scope 属性。您可以使用此属性声明具有该名称的时间轴,而无需实际创建该时间轴。这样一来,使用该名称的时间轴的范围就会更广。在实践中,您可以在共享的父元素上使用 timeline-scope 属性,以便子滚动条的时间轴可以附加到它。

例如:

.parent {
  timeline-scope: --tl;
}
.parent .scroller {
  scroll-timeline: --tl;
}
.parent .scroller ~ .subject {
  animation: animate linear;
  animation-timeline: --tl;
}

在此代码段中:

  • .parent 元素声明了一个名为 --tl 的时间轴。它的任何子项都可以找到它并将其用作 animation-timeline 属性的值。
  • .scroller 元素实际上定义了一个名为 --tl 的滚动时间轴。默认情况下,它只对其子项可见,但由于 .parent 将其设置为 scroll-timeline-root,因此它会附加到 scroll-timeline-root 上。
  • .subject 元素使用 --tl 时间轴。它会沿着其祖先树向上遍历,并在 .parent 上找到 --tl。将 .parent 上的 --tl 指向 .scroller--tl.subject 实际上会跟踪 .scroller 的滚动进度时间轴。

换句话说,您可以使用 timeline-root 将时间轴移至祖先(也称为提升),以便祖先的所有子项都可以访问它。

timeline-scope 属性可与滚动时间轴和查看时间轴搭配使用。

更多演示和资源

如需查看本文中介绍的所有演示,请访问 scroll-driven-animations.style 迷你网站。该网站包含更多演示,旨在展示滚动驱动型动画的各种可能性。

下面的专辑封面列表就是其中一个额外的演示。每个封面在成为焦点时都会以 3D 方式旋转。

演示:封面照片

✨ 亲自试用

或者,您也可以查看此利用 position: sticky 的堆叠卡片演示。随着卡片堆叠,已固定的卡片会缩小,从而产生不错的深度效果。最后,整个堆叠会作为一个群组一起滑出视野。

演示:堆叠卡片

✨ 亲自试用

scroll-driven-animations.style 中还包含一系列工具,例如本博文前面提到的“查看时间轴范围进度”可视化图表。

2023 年 Google I/O 大会的Web 动画新变化主题演讲中也介绍了滚动条驱动的动画。