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-name 或 view-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-timeline 是 scroll-timeline-name 和 scroll-timeline-axis 的简写:
.scroller {
/* 等价于分别设置 */
scroll-timeline: --my-timeline inline;
/* 或者 */
scroll-timeline-name: --my-timeline;
scroll-timeline-axis: inline;
}
view-timeline 简写属性
类似地,view-timeline 是 view-timeline-name、view-timeline-axis 和 view-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();
}
}
性能优化建议
- 使用 transform 和 opacity:滚动驱动动画中的动画属性应主要使用 transform 和 opacity,以利用 GPU 加速
- 避免动画布局属性:height、width、margin 等布局属性会触发重排,不建议在滚动驱动动画中使用
- 使用 linear 时间函数:由于进度由滚动控制,使用 linear 时间函数可以确保动画与滚动的同步性
- 避免在动画中触发样式计算:确保 @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% 的范围内播放 */
}