使用滚动驱动的 CSS 动画来为英雄元素制作动画

当您导航到 github.com 时(您必须退出,否则您将进入 GitHub 应用程序),您会看到此登录页面。

这是一个精心设计的居中英雄设计,带有注册框。向下滚动时,英雄文本会逐渐消失并移到代码编辑器后面,这很引人注目。如何实现这样的效果?

我很好奇,于是快速进行了一次逆向工程,结果发现一切都是用 React 构建的,应用程序会监听 `scroll` 事件。滚动时,JS 会在 `requestAnimationFrame` 循环中更新英雄文本的 `scale` 和 `opacity` 属性。简而言之,要创建这种效果需要做很多事情!

在本文中,我将解释如何**使用现代(和未来)CSS 和零 JavaScript 实现类似的效果**。

一些准备工作

为了准备好一些代码,我在 Figma 中快速设计了一个英雄部分,并使用 Builder Figma 插件通过 Visual Copilot 生成了一些 React 代码。

使用 position: sticky 将英雄图像滑动到文本上

重新创建 GitHub 效果的合理第一步是将英雄图像滑动到英雄文本上。

为此,我们可以在滚动时将英雄文本固定在顶部。通过使用 `position: sticky`,我们可以将元素放置在容器内,并在人们滚动时“使其固定在”某个位置。

为了给英雄文本一些空间,我们还将使用 top CSS 属性添加一些空间。

.hero-text {
  /* Make the element sticky */
  position: sticky;
  /* Give the element some space at the top */
  top: 1rem;
}

当我们添加 `position: sticky` 时,我们会发现英雄文本会滚动到其他滚动元素之上。这正是您在大多数粘性场景中想要的结果,但在我们的例子中,我们希望粘性元素位于其余元素之下。

要使英雄文本“滑入下方”,我们需要为其他元素创建一个新的堆叠上下文。有很多方法可以做到这一点,但我喜欢设置“isolation:isolate”来创建一个新的堆叠上下文,因为它是一个漂亮的单行代码,而且应该有更多人了解“isolation”CSS 属性。

.hero-image {
  /* Create a new stacking context */
  isolation: isolate;
}

瞧瞧——看看这个!

这还不错,不是吗?现在,我们只需要添加滚动淡入淡出行为。我们该如何添加它?

添加仅 CSS 滚动驱动的动画

如果您正在阅读此博客,那么您可能已经了解了 CSS 的持续发展。让我告诉您,现在正是成为 Web 开发人员的最佳时机。列举几个最近的 CSS 功能:我们现在有了容器查询、`:has()` 选择器进入了舞台,并且视图转换使动画制作比以往任何时候都更容易。

现代网络非常棒,另一个令人兴奋的功能是滚动驱动的 CSS 动画。滚动什么?是的,你没听错!

什么是滚动驱动的 CSS 动画?

滚动驱动动画允许您删除所有这些自定义 JavaScript 滚动处理程序并使用 CSS 动画 API 添加滚动动画行为。

该 API 在主线程上运行,与 JavaScript 领域中成千上万个触发滚动处理程序相比,性能大幅提升。借助滚动驱动的 CSS 动画,您可以用更少、更高效的代码创建滚动效果。

新的 CSS 功能目前仅受 Chromiums 支持,但 Webkit 对此平台新增功能开放,Firefox 也通过标志发布了初始实现!原生滚动驱动动画即将登陆网络!

如果仅使用 Chromium 还不足以使用新的 CSS 功能,我理解。但是,基于 Chromium 的浏览器在全球的市场份额超过 70%,将滚动效果视为渐进式增强是合理的。支持滚动驱动动画的浏览器将向用户展示一些视觉效果,而其他浏览器只会在英雄图像上方呈现标题。

但是让我们开始吧,并添加一些视觉享受!

添加你的第一个滚动驱动动画

滚动驱动的动画基于古老的“@keyframes”。

@keyframes fade-out {
  0% {
    opacity: 1;
    scale: 1;
  }

  100% {
    opacity: 0;
    scale: 0.5;
  }
}

我们可以定义一个“淡出”动画,通过缩小元素并降低其不透明度来使元素消失。接下来,我们采用“hero-text”类并设置滚动驱动动画。

.hero-text {
  /* Set the animation */
  animation: fade-out linear;
  /* Connect the animation to the nearest scroller's scroll progress */
  animation-timeline: scroll();
}

看看这个:两个添加的 CSS 声明已经为我们提供了可以开展工作的基础。

让我们了解这里发生了什么!

`hero-text` 类现在使用 `animation` 属性设置 `fade-out` 关键帧。不过,它并不是一个“普通”的 `animation` 声明。

`animation` 简写没有定义 `animation-duration`。通常,时间控制关键帧动画进度,但在我们的例子中,`animation-duration` 设置为 `auto`,因为我们希望它依赖于滚动进度和滚动时间轴。

在 `animation` 声明下方,您将看到新的 `animation-timeline` 属性。理论上,`animation-timeline` 也是 `animation` 简写的一部分,但您只能使用它来重置时间线。要定义除 `auto` 之外的动画,我们必须定义动画,然后定义指定的 `animation-timeline`。在这种情况下,顺序很重要!

但我问得有点过了;动画时间线是什么?

指定动画时间轴

**动画时间轴指定用于控制 CSS 关键帧动画进度的时间轴**。通常,您可以通过映射到 `animation-timeline: auto` 的时间来控制动画。但对于滚动驱动的动画,现在有两个新的动画滚动时间轴:滚动进度(`animation-timeline: scroll()`)和视图进度(`animation-timeline: view()`)时间轴。

在滚动进度上为英雄元素添加动画(scroll())

使用 **scroll() CSS 函数定义的滚动进度时间轴将包装滚动容器**(也称为滚动条)的滚动进度映射到从 `0%` 到 `100%` 的值,并将它们应用于关键帧动画。在我们的例子中,最近的滚动容器是 `html` 元素。向下滚动时,整个文档的滚动进度控制 `淡出` 动画。

尽管这种方法可行,但您现在可能想知道如果 HTML 文档非常长会发生什么情况。

如果您想显示文档滚动进度指示器,则 `scroll()` 会派上用场,但对于我们的英雄动画,使用滚动进度时间轴是没有意义的。如果您在 2000px 高的文档中向下滚动 500px,动画将进展 25%。但当文档增长到 10000px 时,滚动时间轴只会进展到 5%。动画几乎不会被注意到。我们的动画应该依赖于文档长度以外的其他东西!

幸运的是,我们可以使用另一个滚动时间线。

在视图进度上为英雄元素添加动画 (view())

如果您不想考虑整体滚动进度,还有另一种控制滚动驱动动画的方法。**新的 view() CSS 函数允许您考虑滚动条内元素的可见性**。`view()` 函数非常复杂,但最重要的是,视图进度时间线也非常强大!

我将只在这里介绍一些细节(如果您想了解更多,请查看下面的资源),但让我们退一步来重新思考视觉效果应该如何发挥作用。

我们希望:

  • 当我们的英雄开始移出屏幕时,从 0% 开始关键帧动画。
  • 当英雄消失时,在 100% 处完成关键帧动画。
  • 为了实现此行为,我们必须调整动画范围,当然,还有另一个新的 CSS 属性 - `animation-range`。`animation-range` 使我们能够定义滚动驱动动画的开始和结束。对于我们的 `fade-out` 动画,我们希望在元素开始消失(“0%”)直到完全消失(“100%”)时启动动画。

    对于此用例,我们可以添加退出动画范围。让我们继续将其放入 CSS 中。

    .hero-text {
      animation: fade-out auto ease-in-out;
      animation-timeline: view();
      /* Apply a custom animation range */
      animation-range: exit 0% exit 100%;
    }

    当您添加自定义动画范围时,您会发现,对于我们的英雄动画,它不起作用。

    我们将英雄文本定义为粘性的,这种定位意味着只有当周围的容器移出屏幕时,它才会离开滚动条。英雄文本动画已应用,但它不可见,因为我们的英雄虚拟图像覆盖了滚动退出动画。

    当我们移除粘性时,我们会发现我们的“动画范围”可以正常工作。现在我们想为粘性元素设置动画,我们该怎么做?

    附加命名视图进度时间轴来为粘性元素设置动画

    当然,新的滚动驱动动画也为这个问题提供了解决方案。为了使我们的动画可见,我们需要使用命名视图进度时间轴根据另一个元素的视图进度来控制动画。

    在我们的 CSS 中,我们可以进入包装英雄容器并指定一个 `view-timeline-name`...

    .hero-container {
      /* Define a named view progress timeline */
      view-timeline-name: --hero-scroll;
    }

    ...然后将我们的英雄文本更改为使用视图时间线而不是“view()”。

    .hero-text {
      animation: fade-out auto ease-in-out;
      /* Specify the container animation timeline */
      animation-timeline: --hero-scroll;
      animation-range: exit 0% exit 100%;
    }

    现在看看这个!

    当周围的容器开始离开屏幕时,我们的英雄文本元素会淡出并缩小。这需要我们编写多少 CSS?只有几个 CSS 声明和一个关键帧动画。我很惊讶!

    不过,我们还要考虑一些事情,以便成为优秀的网络公民。

    注意浏览器支持和用户偏好

    首先,有些人在面对屏幕上的意外动作时会感到不适甚至恶心。为了构建,我们可以添加 CSS“reduced-motion”媒体查询,以允许人们选择退出移动动画。

    其次,在创建演示时,我发现 Firefox 的标记实现尚不支持“animation-range”属性。为了解决这个问题,我们还可以添加“@supports”功能查询,以避免在尚不支持滚动驱动动画的浏览器中出现动画中断。

    如果您问我,后备看起来也很棒,但是当然,如​​果您不喜欢图像在文本上滑动,您也可以消除粘性。

    因此,这是最终的代码。

    @supports (animation-range: exit) {
      @media (prefers-reduced-motion: no-preference) {
        .hero-container {
          view-timeline-name: --hero-scroll;
        }
    
        .hero-text {
          animation: fade-out auto ease-in-out;
          animation-timeline: --hero-scroll;
          animation-range: exit 0% exit 100%;
        }
      }
    }
    
    .hero-text {
      position: sticky;
      top: 1rem;
    }
    
    .hero-image {
      isolation: isolate;
    }
    
    @keyframes fade-out {
      0% {
        opacity: 1;
        scale: 1;
      }
    
      100% {
        opacity: 0;
        scale: 0.5;
      }
    }

    这难道不神奇又有趣吗?我非常喜欢它!

    结论

    因此,我们仅用几行现代 CSS 就重建了 GitHub 登陆页面上这个 JavaScript 密集型效果。我们的滚动动画现在更易于维护且性能更高,因为我们不依赖任何会阻塞主线程的 JavaScript!

    而且由于我们依靠现代 CSS 与渐进式增强相结合,如果用户的浏览器支持,我们可以为用户提供最佳体验。双赢!

    如果您想看到实际效果,这里是已部署的版本。

    你已经上瘾了吗?我确实上瘾了!

    后续步骤Next steps

    如果您想了解有关滚动驱动动画的更多信息,可以去一个地方。Chrome DevRel 团队的 Bramus(又名“ScrollAnimation 先生”)维护着 scroll-driven-animations.style。该网站列出了许多工具、资源和视觉示例。

    如果您更喜欢通过视频学习,Bramus 还在 YouTube 上发布了免费课程。

    如果你想知道如何调试滚动驱动的动画,这个 Chrome 扩展程序对于弄清楚发生了什么非常有用。

    现在我们已经完成了,祝您动画制作愉快,并让我知道进展如何!

    作者

    斯蒂芬·朱迪斯

    Stefan 热衷于研究网络性能、新技术和可访问性,每周发送网络开发新闻通讯,并喜欢在他的博客上分享奇思妙想。