CSS 滚动驱动动画

深入理解 CSS 滚动驱动动画机制:scroll() 和 view() 函数、animation-timeline 属性、命名滚动时间线和视图时间线、以及 scroll-timeline 详细用法。

CSS 滚动驱动动画

为什么滚动驱动动画是现代交互的核心

传统的 CSS 动画依赖于时间轴(timeline),动画的进度由时间来决定。但在现代 Web 应用中,滚动驱动动画(Scroll-Driven Animations)允许动画的进度与滚动位置挂钩——当用户滚动页面时,动画随之播放。这为创造流畅的交互体验提供了强大的能力。

想象一下:一个阅读进度条随着页面滚动而填满,一个元素在进入视口时淡入并放大,一个导航栏在滚动时改变样式——这些效果都可以通过滚动驱动动画实现,而无需 JavaScript。

理解滚动驱动动画的机制,对于构建高性能、高交互性的现代 Web 应用至关重要。

面试定位:滚动驱动动画是 CSS 的高级特性,涉及到动画时间轴的深层概念。面试官通过候选人对 scroll() 和 view() 函数的理解、对 scroll-timeline 和 view-timeline 的区分、以及实际应用场景的掌握,来评估其 CSS 动画和交互设计的深度。


animation-timeline 属性

animation-timeline 是滚动驱动动画的核心属性,它指定了控制动画进度的时间轴类型。默认情况下,动画使用文档的默认 DocumentTimeline(基于时间),但通过 scroll() 和 view() 函数,可以将其改为滚动驱动。

基本语法

.element {
  /* 默认:基于时间的时间轴 */
  animation-timeline: auto;

  /* 无时间轴:动画不播放 */
  animation-timeline: none;

  /* 滚动进度时间轴 */
  animation-timeline: scroll();

  /* 视图进度时间轴 */
  animation-timeline: view();
}

与 animation 简写属性的关系

/* animation 简写会重置 animation-timeline 为 auto */
.element {
  animation: slide 1s ease-in-out;
  animation-timeline: scroll(); /* 这个会失效 */
}

/* 正确做法:先写 animation,再写 animation-timeline */
.correct-order {
  animation: slide 1s ease-in-out;
  animation-timeline: scroll(); /* 在 animation 之后声明才有效 */
}

声明顺序的重要性

CSS 动画属性之间存在相互影响的关系。animation 简写属性会重置所有未明确指定的子属性为默认值。当你在 animation 之后设置 animation-timeline,时间轴才能正常工作。

/* 错误顺序 */
.wrong {
  animation-timeline: scroll();
  animation: slide 1s ease-in-out; /* 这会重置 animation-timeline 为 auto */
}

/* 正确顺序 */
.correct {
  animation: slide 1s ease-in-out;
  animation-timeline: scroll(); /* 在 animation 之后声明 */
}

scroll() 函数:滚动进度时间轴

scroll() 函数创建一个滚动进度时间轴(Scroll Progress Timeline),动画的进度由滚动位置决定。当滚动容器被滚动时,动画从 0% 移动到 100%。

这个函数的工作原理是:浏览器会找到指定的滚动容器(scroller),然后将动画的进度与该容器的滚动位置关联起来。当用户滚动时,动画随之播放,进度值从滚动容器顶部(或左侧)的 0% 变化到底部(或右侧)的 100%。

语法详解

.animation {
  animation-timeline: scroll();
  animation-timeline: scroll(root);          /* 使用视口作为滚动容器 */
  animation-timeline: scroll(nearest);      /* 使用最近的可滚动祖先元素 */
  animation-timeline: scroll(self);          /* 使用元素自身作为滚动容器 */

  /* 指定滚动轴 */
  animation-timeline: scroll(block);         /* 块轴方向(默认) */
  animation-timeline: scroll(inline);       /* 内联轴方向 */
  animation-timeline: scroll(x);            /* 水平滚动 */
  animation-timeline: scroll(y);            /* 垂直滚动 */

  /* 组合参数 */
  animation-timeline: scroll(inline nearest); /* 内联轴 + 最近祖先 */
}

参数说明

参数 描述
<scroller> root, nearest, self 指定使用哪个滚动容器
<axis> block, inline, x, y 指定滚动轴方向

scroll(root) 的工作原理

scroll(root) 使用视口(viewport)作为滚动容器。当用户在视口中滚动时,动画的进度从页面顶部的 0% 变化到底部的 100%。这非常适合创建页面级别的进度指示器。

.page-progress {
  animation: progress-fill linear;
  animation-timeline: scroll(root);
}

@keyframes progress-fill {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

scroll(nearest) 的工作机制

scroll(nearest) 使用距离元素最近的滚动祖先元素作为滚动容器。这使得动画能够响应局部滚动,而不是整个页面的滚动。当你在一个可滚动的卡片内部放置一个需要动画的元素时,这种方式特别有用。

.card-inner {
  animation: reveal linear;
  animation-timeline: scroll(nearest);
}

实用示例:进度条

.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: linear-gradient(90deg, #667eea, #764ba2);
  transform-origin: left center;
  animation: grow linear;
  animation-timeline: scroll(root);
}

@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

实用示例:旋转元素

.scrolling-rotate {
  animation: rotate 1ms linear;
  animation-timeline: scroll(inline nearest);
}

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

view() 函数:视图进度时间轴

view() 函数创建一个视图进度时间轴(View Progress Timeline),动画的进度由元素在滚动容器中的可见性决定。当元素进入滚动容器时,动画从 0% 开始;当元素完全离开滚动容器时,动画达到 100%。

这个机制使得"元素进入视口时触发动画"这样的效果成为可能,无需 JavaScript。

语法详解

.view-animation {
  animation-timeline: view();
  animation-timeline: view();                 /* 默认:块轴 */
  animation-timeline: view(inline);          /* 内联轴 */
  animation-timeline: view(x);               /* 水平轴 */
  animation-timeline: view(y);              /* 垂直轴 */

  /* 视图插入值 */
  animation-timeline: view(auto);             /* 自动计算插入 */
  animation-timeline: view(100px);           /* 固定插入值 */
  animation-timeline: view(10% 20%);         /* 起始和结束插入 */
}

工作机制

视图进度时间轴的核心概念是入口边出口边

滚动容器
┌─────────────────────────┐
│                         │
│    ┌───────────┐        │  ← 元素进入(出口边接触入口边):0%
│    │  Element  │        │
│    └───────────┘        │
│                         │
│                         │
│                         │
│    ┌───────────┐        │  ← 元素离开(入口边接触出口边):100%
│    │  Element  │        │
│    └───────────┘        │
│                         │
└─────────────────────────┘

时间线的 0% 对应元素首次开始进入滚动容器的时刻,100% 对应元素完全离开滚动容器的时刻。在这两个时刻之间,动画会随着元素在滚动容器中的位置变化而变化。

视图插入值(view-timeline-inset)

视图插入值允许你调整"元素进入"和"元素离开"的触发时机:

.adjusted-view {
  animation-timeline: view();
  /* 使用自动插入 */
  view-timeline-inset: auto;
}

.adjusted-view-2 {
  animation-timeline: view();
  /* 固定插入:元素进入时多等 100px */
  view-timeline-inset: 100px;
}

.adjusted-view-3 {
  animation-timeline: view();
  /* 不对称插入 */
  view-timeline-inset: 10% 20%;
  /* 起始插入 10%,结束插入 20% */
}

实用示例:淡入效果

.fade-in-on-scroll {
  animation: fade-in linear;
  animation-timeline: view();
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

实用示例:缩放效果

.scale-on-scroll {
  animation: scale-effect 1ms linear;
  animation-timeline: view();
}

@keyframes scale-effect {
  0% {
    transform: scale(0.8);
    opacity: 0.5;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

命名时间线

除了匿名函数,CSS 还支持命名时间线,允许更精确地控制滚动容器和动画元素的关系。命名时间线通过 scroll-timeline-nameview-timeline-name 定义,通过 animation-timeline 引用。

命名滚动时间线

/* 定义命名滚动时间线 */
.scroller {
  scroll-timeline-name: --square-timeline;
  scroll-timeline-axis: inline; /* 水平滚动 */
}

/* 使用命名时间线 */
.square {
  animation: rotate 1ms linear;
  animation-timeline: --square-timeline;
}

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

命名视图时间线

命名视图时间线使用 view-timeline-name

/* 自身作为滚动容器并定义视图时间线 */
.subject {
  view-timeline-name: --self-timeline;
  animation: animate linear;
  animation-timeline: --self-timeline;
}

@keyframes animate {
  from { opacity: 0; transform: scale(0.5); }
  to { opacity: 1; transform: scale(1); }
}

/* 父容器作为滚动容器 */
.container {
  view-timeline-name: --parent-timeline;
}

.parent {
  animation: animate linear;
  animation-timeline: --parent-timeline;
}

scroll-timeline 简写属性

scroll-timelinescroll-timeline-namescroll-timeline-axis 的简写:

.scroller {
  /* 等价于分别设置 */
  scroll-timeline: --my-timeline inline;
  /* 或者 */
  scroll-timeline-name: --my-timeline;
  scroll-timeline-axis: inline;
}

view-timeline 简写属性

类似地,view-timelineview-timeline-nameview-timeline-axisview-timeline-inset 的简写:

.subject {
  view-timeline: --self-timeline block auto;
  /* 或者展开形式 */
  view-timeline-name: --self-timeline;
  view-timeline-axis: block;
  view-timeline-inset: auto;
}

scroll-timeline 详细属性

scroll-timeline-name

定义滚动容器的名称,供 animation-timeline 引用:

.scroller {
  scroll-timeline-name: --page-progress;
  scroll-timeline-name: none; /* 默认值 */
}

名称必须是自定义标识符(以 -- 开头),这确保了命名空间不会与原生 CSS 属性冲突。

scroll-timeline-axis

定义滚动轴方向:

.scroller {
  scroll-timeline-axis: block;   /* 默认:块轴方向(通常是垂直) */
  scroll-timeline-axis: inline;  /* 内联轴方向(通常是水平) */
  scroll-timeline-axis: x;       /* 水平滚动 */
  scroll-timeline-axis: y;       /* 垂直滚动 */
}

view-timeline 详细属性

view-timeline-name

定义视图时间线的名称:

.subject {
  view-timeline-name: --reveal-animation;
  view-timeline-name: none; /* 默认值 */
}

view-timeline-axis

定义视图可见性计算的轴:

.subject {
  view-timeline-axis: block;   /* 默认:块轴 */
  view-timeline-axis: inline;  /* 内联轴 */
  view-timeline-axis: x;       /* 水平轴 */
  view-timeline-axis: y;      /* 垂直轴 */
}

view-timeline-inset

定义视图时间线的插入值,调整元素进入和离开视图的时刻:

.subject {
  view-timeline-inset: auto;              /* 自动计算 */
  view-timeline-inset: 100px;             /* 固定值 */
  view-timeline-inset: 10% 20%;           /* 起始和结束 */
  view-timeline-inset: 0 100px;           /* 起始0,结束100px */
}

插入值可以是长度、百分比或 auto。正值会延迟元素离开的时间,负值则会提前触发。


实战场景

场景一:阅读进度指示器

这是滚动驱动动画最常见的应用之一。进度条随着用户滚动页面而填充,直观地显示阅读进度。

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: linear-gradient(90deg, #667eea, #764ba2);
  z-index: 1000;
  transform-origin: 0 50%;
  animation: progress-grow linear;
  animation-timeline: scroll(root block);
}

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

场景二:滚动时固定头部变化

导航栏在用户滚动时改变样式,从透明变为白色背景并添加阴影,这是现代网站常见的效果。

.header {
  position: sticky;
  top: 0;
  padding: 1rem;
  background: transparent;
  transition: background 0.3s, box-shadow 0.3s;
  animation: header-shrink linear;
  animation-timeline: scroll(root);
  animation-range: 0 100px;
}

@keyframes header-shrink {
  from {
    background: transparent;
    box-shadow: none;
  }
  to {
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  }
}

场景三:元素进入视口动画

当元素进入视口时播放动画,实现"滚动揭示"效果。

.reveal-element {
  animation: reveal linear;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(50px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

场景四:画廊卡片堆叠效果

在滚动时,卡片产生视觉上的堆叠和缩放效果。

.gallery-card {
  animation: stack linear both;
  animation-timeline: view(y);
}

@keyframes stack {
  0% {
    transform: scale(1);
    z-index: 1;
  }
  50% {
    transform: scale(0.95) translateY(-10px);
    z-index: 2;
  }
  100% {
    transform: scale(0.9) translateY(-20px);
    z-index: 3;
  }
}

场景五:视差滚动效果

通过在不同滚动容器中使用滚动驱动动画,可以实现视差效果。

.parallax-bg {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  animation: parallax-move linear;
  animation-timeline: scroll(root);
}

@keyframes parallax-move {
  from { transform: translateY(0); }
  to { transform: translateY(30%); }
}

浏览器支持与注意事项

当前支持状态

滚动驱动动画是 CSS 的实验性特性,主要在 Chromium 内核浏览器(Chrome、Edge、Samsung Internet)中得到支持。Firefox 和 Safari 尚未实现这些特性。

使用时应注意渐进增强:

/* Feature query */
@supports (animation-timeline: scroll()) {
  .element {
    animation-timeline: scroll();
  }
}

性能优化建议

  1. 使用 transform 和 opacity:滚动驱动动画中的动画属性应主要使用 transform 和 opacity,以利用 GPU 加速
  2. 避免动画布局属性:height、width、margin 等布局属性会触发重排,不建议在滚动驱动动画中使用
  3. 使用 linear 时间函数:由于进度由滚动控制,使用 linear 时间函数可以确保动画与滚动的同步性
  4. 避免在动画中触发样式计算:确保 @keyframes 中的属性不会触发布局或重绘

与 JavaScript 动画的比较

方面 CSS 滚动驱动动画 JavaScript 动画
性能 高(运行在 compositor 线程) 中等(可能触发重排)
代码量 少(纯 CSS) 多(需要计算逻辑)
精确控制 有限 完全控制
浏览器支持 实验性 广泛支持
调试难度 较高 较低(可使用断点)

animation-range 属性

animation-range 是滚动驱动动画的重要补充,允许你控制动画在时间轴的哪个范围内播放:

.range-example {
  animation: reveal linear;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
  /* 动画只在 0% 到 40% 的范围内播放 */
}

延展阅读