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

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

滚动条驱动的动画

浏览器支持

  • 115
  • 115
  • x

来源

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

文档上方的阅读指示器,由滚动驱动。

类似的滚动驱动型动画是一种与元素在其滚动容器中的位置相关联的动画。例如,使用它时,元素在进入视野中时可以淡入。

此页面上的图片在进入视图时淡入。

实现这些效果的典型方式是在主线程上响应滚动事件,这会导致两个主要问题:

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

这使得创建与滚动保持同步的高性能滚动驱动动画不可能实现或非常困难。

Chrome 115 版开始提供一组新的 API 和概念,可用于启用声明式滚动驱动动画:滚动时间轴和查看时间轴。

这些新概念可与现有的 Web Animations API (WAAPI)CSS Animations API 集成,从而继承这些现有 API 带来的优势。这包括使滚动驱动的动画在主线程之外运行的能力。没错,没错,理解得对:现在,只需几行额外的代码,你就可以拥有丝滑的动画,由滚动驱动,在主线程之外运行。不喜欢什么?!

网页动画 - 简短回顾

使用 CSS 在网页中制作动画

若要在 CSS 中制作动画,请使用 @keyframes @ 规则定义一组关键帧。使用 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 开始,随着时钟时间的推移,开始逐渐向前跳动。这是默认的动画时间轴,到目前为止,它是您能访问的唯一动画时间轴。

滚动驱动的动画规范定义了两种新的时间轴类型:

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

滚动进度时间轴

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

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

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

✨ 亲自试用

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

查看进度时间轴

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

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

查看进度时间轴从主题开始与滚动条相交开始,到主题停止与滚动条交叉时结束。在下面的可视化图表中,您可以看到,当对象进入滚动容器时,进度从 0% 开始从 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)

演示:阅读进度指示器

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

演示:阅读进度指示器

✨ 亲自试用

使用“位置固定”时,阅读进度指示器位于页面顶部。为了利用复合动画,系统不会为 width 添加动画效果,而是使用 transform 在 x 轴上缩小元素。

<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% 开始,表示您当前正在查看图片中的第 3 张图片。当最后一张图片在视图中(由滚动条滚动到末尾)确定时,指示器会占据滚动条的整个宽度。名为“Scroll Progress Timeline”的动画用于驱动动画。

演示:水平轮播步骤指示器

✨ 亲自试用

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

<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 的滚动位置,可以为其添加动画效果。可通过引用名为“Scroll Progress Timeline”的 --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,
});

如需将其附加到网页动画,请将其作为 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> 与滚动进度时间轴中的相同,用于定义要跟踪的轴。默认值为 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();
}

Intermezzo:查看时间轴范围

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

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

设置为跟踪拍摄正文的条目范围的视图时间轴。仅当对象进入滚动端口时,动画才会运行。

您可以定位的可能的视图时间轴范围如下:

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

若要定义范围,您必须设置范围起始值和范围结束值。每个对象均包含范围名称(参见上文的列表)和范围偏移量,以确定在该范围名称内的位置。范围偏移量通常为介于 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%',
  }
);

使用下面嵌入的工具查看每个范围名称所代表的含义以及百分比如何影响开始和结束位置。尝试将范围起始值设为 entry 0%,将范围结束值设为 cover 50%,然后拖动滚动条以查看动画结果。

View Timeline Ranges Visualizer(视图时间轴范围可视化工具),可通过以下网址获取:https://goo.gle/view-timeline-range-tool

观看录音

在玩转这些“查看时间轴范围”工具时,您可能会注意到,有些范围可以通过两个不同的范围名称 + 范围偏移组合来定位。例如,entry 0%entry-crossing 0%cover 0% 都会定位到同一区域。

如果范围起始值和范围结束值定位到相同的范围名称并跨越整个范围(从 0% 到 100%),您可以将值缩短为仅包含范围名称。例如,animation-range: entry 0% entry 100%; 可以重写为更短的 animation-range: entry

演示:图片展示

此演示在图片进入滚动窗口时淡入。此操作通过匿名视图时间轴来完成。调整了动画范围,使每张图片在滚动至滚动条一半时保持完全不透明。

演示:图片展示

✨ 亲自试用

展开效果可通过使用动画裁剪路径来实现。用于此效果的 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 中创建已命名的“查看进度时间轴”

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

系统将应用相同类型的值,并应用相同的查找已命名时间轴的规则。

演示:图片展示、重温

通过重新修改之前的图像显示演示,修改后的代码如下所示:

.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 创建视图时间轴,请创建 ViewTimeline 类的一个新实例。传入一个包含您要跟踪的 subjectaxisinset 的属性包。

  • subject:对要在自己的滚动条内跟踪的元素的引用。
  • axis:要跟踪的轴。与 CSS 变体类似,可接受的值为 blockinlinexy
  • inset:确定框是否位于视图中时,滚动端口的边衬区(正值)或偏移值(负值)
const tl = new ViewTimeline({
  subject: document.getElementById('subject'),
});

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

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

✨ 亲自试用

更多可尝试的功能

附加到多个视图时间轴范围(包含一组关键帧)

我们来看一下这个联系人列表演示,其中列表条目带有动画效果。当列表条目从底部进入滚动窗口时,它会滑动并淡入;当它退出顶部的滚动窗口时,它将滑出并淡出。

演示:联系人列表

✨ 亲自试用

在本演示中,每个元素都装饰有一个视图时间轴,该时间轴可在元素跨过其滚动窗口时跟踪该元素,但其上还附加了两个滚动驱动的动画。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,因此它会附加到子级。
  • .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 上还介绍了一系列工具,例如本博文前面部分介绍的“查看时间轴范围进度 (View Timeline Range Progress)”可视化图表。

滚动条驱动的动画也在 2023 年 Google I/O 大会上的网页动画的新变化中进行了介绍。