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

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

发布日期:2023 年 5 月 5 日

滚动驱动的动画

Browser Support

  • Chrome: 115.
  • Edge: 115.
  • Firefox: behind a flag.
  • Safari: 26.

Source

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

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

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

本页面的图片在进入视图时会淡入。

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

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

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

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

这些新概念与现有的 Web Animations API (WAAPI)CSS Animations API 集成,使它们能够继承这些现有 API 带来的优势。这包括让滚动条驱动的动画在主线程以外运行的功能。没错,您没看错:现在只需添加几行额外的代码,即可实现由滚动驱动的流畅动画,且动画在主线程之外运行。有什么理由不喜欢呢?

网页上的动画,简要回顾

使用 CSS 在网页上制作动画

如需在 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 在网页上添加动画

在 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%。

✨ 亲自尝试一下

查看进度时间轴通常简称为“查看时间轴”。您可以根据拍摄对象的尺寸来定位查看时间轴的特定部分,但我们稍后会详细介绍。

Scroll Progress Timelines 实践

在 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% 开始,表示您目前正在查看三张图片中的第一张。当滚动条滚动到末尾时,指示器会占据滚动条的整个宽度。命名的滚动进度时间轴用于驱动动画。

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

✨ 亲自尝试一下

图库的基本标记如下:

<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 会跟踪根滚动条,并在您滚动网页时将 #progress 在 x 轴上从 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();
}

中场休息:查看时间轴范围

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

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

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

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

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

如需定义范围,您必须设置 range-start 和 range-end。每个都包含范围名称(请参阅上文中的列表)和范围偏移量,用于确定在该范围名称内的位置。范围偏移量通常是介于 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

观看录制内容

您在试用此“查看时间轴范围”工具时可能会注意到,某些范围可以通过两种不同的 range-name + range-offset 组合来定位。例如,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 中创建视图时间轴,请创建 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%',
});

✨ 亲自尝试一下

更多可尝试的操作

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

我们来看一下这个联系人列表演示,其中的列表条目带有动画效果。当列表条目从底部进入滚动端口时,会以滑动加淡入的方式显示;当列表条目从顶部退出滚动端口时,会以滑动加淡出的方式隐藏。

演示:联系人列表

✨ 亲自尝试一下

在此演示中,每个元素都使用一个视图时间轴进行装饰,该时间轴会跟踪元素在滚动视口中的移动情况,但同时有两个滚动驱动的动画附加到该元素。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,因此它会附加到 .parent
  • .subject 元素使用 --tl 时间轴。它会遍历其祖先树,并在 .parent 上找到 --tl。如果 .parent 上的 --tl 指向 .scroller--tl,则 .subject 实际上会跟踪 .scroller 的滚动进度时间轴。

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

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

更多演示和资源

本文中介绍的所有演示均可在 scroll-driven-animations.style 微型网站上找到。该网站还包含更多演示,可突出展示滚动驱动动画的强大功能。

其中一个额外的演示是此专辑封面列表。每张封面都会在 3D 空间中旋转,成为焦点。

演示:Cover Flow

✨ 亲自尝试一下

或者,您也可以查看利用 position: sticky 的堆叠卡片演示。随着卡片堆叠,已卡住的卡片会缩小,从而营造出不错的深度效果。最后,整个堆栈会作为一个整体滑出视图。

演示:堆叠卡片

✨ 亲自尝试一下

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

Google I/O ’23 大会上的网络动画的新动态也介绍了滚动条驱动的动画。