前端开发中有一个经典需求:让元素的动画随着页面滚动而播放。比如文章顶部的阅读进度条、长页面的渐现卡片、跟随滚动旋转的元素等等。以前要实现这类效果,JavaScript是绝对的主力——监听scroll事件,计算滚动比例,然后手动更新元素样式。这种方式不仅代码难维护,而且在性能敏感的场景下还容易出现卡顿感。
现在,CSS给了我们一套原生解决方案:滚动驱动动画(Scroll-Driven Animations)。通过animation-timeline属性和对应的scroll-timeline或view-timeline,我们可以直接把元素的动画进度绑定到滚动条的位置上,让浏览器在合成线程上就完成这一切,性能更好,写法也更优雅。这一篇就来把它从原理到实战彻底讲清楚。
核心概念:把滚动条当成动画的播放头
传统的CSS动画基于时间轴:@keyframes定义动画在0%到100%之间的状态,animation-duration决定播放时长。而滚动驱动动画把这条时间轴换成了滚动进度——滚动条从0%滚到100%,动画也随之从0%执行到100%。
这个转变依赖两个新属性:
animation-timeline:指定动画使用哪条滚动时间线。scroll-timeline(或view-timeline):定义一条滚动时间线,指明以哪个滚动容器为参照,以及滚动到什么位置对应动画的起点和终点。
有趣的是,虽然动画本身是绑定在元素上的,但控制进度的却是另一个滚动容器。这意味着你可以让一个固定定位的进度条,根据页面的整体滚动进度来伸缩。
浏览器支持与降级
截至目前,Chrome 115+ 和 Edge 115+ 已经完整支持滚动驱动动画,Firefox 也通过 layout.css.scroll-driven-animations.enabled 标志位提供了实验性支持。Safari 的进度稍慢,但整体方向已经明确。在生产环境中,可以使用特性检测提供简单的降级,例如:
@supports (animation-timeline: scroll()) {
/* 使用滚动驱动动画 */
}
实战案例一:阅读进度条
这是最常见的一个例子——固定在页面顶部的进度条,随着用户向下滚动文章而变宽。
HTML结构非常简单:
<div class="progress-bar"></div>
<main>
<!-- 长文章内容 -->
</main>
然后利用匿名滚动进度时间线来定义动画,这是最便捷的写法,不需要为容器单独命名时间线:
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, #6c5ce7, #a29bfe);
transform-origin: left center;
animation: grow 1s linear;
animation-timeline: scroll(root);
}
这里的关键是animation-timeline: scroll(root);。它定义了一条滚动时间线,滚动容器是根元素(即<html>或document.scrollingElement)。当根元素从顶部滚到底部时,动画从from执行到to。而且animation-duration在这里其实被替换了,但浏览器仍然需要一个占位值,所以我们保留1s。
这个进度条不需要一行JavaScript,并且因为动画由浏览器直接在合成线程处理,性能极佳。
实战案例二:渐现卡片(视图进度时间线)
另一个常见模式:卡片元素进入视口时逐渐显示,滚出时再淡出。这可以用视图进度时间线(view-timeline)来实现。
HTML:
<div class="card-list">
<div class="card">卡片 1</div>
<div class="card">卡片 2</div>
<div class="card">卡片 3</div>
</div>
CSS部分:
@keyframes card-in {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: card-in 1s linear both;
animation-timeline: view();
}
这里使用了view()函数,它创建了一条基于元素的视图进度时间线。默认情况下,动画会在元素从进入视口开始,到完全离开视口时结束。也就是说,当卡片进入视口底部,动画开始播放;卡片到达视口顶部时,动画播放完毕。元素在视口中的位置直接对应动画进度。
我们还可以精确控制动画的起止范围:
.card {
animation-timeline: view(block 80% 20%);
}
这表示当元素顶部距离视口底部还有80%元素高度时动画开始,当元素底部距离视口顶部还有20%元素高度时动画结束。这样一来,卡片在视口中更早地完全显现,避免进入视口后很久才完成动画。
实战案例三:水平滚动故事线
有时候,我们要的不是垂直滚动驱动元素本身的动画,而是驱动一个水平滑动的时间轴。配合scroll-timeline命名时间线,可以让多个元素共享同一条进度。
<div class="story-container">
<div class="story-track">
<section>第一章</section>
<section>第二章</section>
<section>第三章</section>
</div>
</div>
先定义命名的滚动时间线:
.story-container {
overflow-x: auto;
scroll-timeline-name: --story;
scroll-timeline-axis: x;
}
然后为内部的章节设置一个“跃入”动画,并将其绑定到该时间线:
@keyframes slide-in {
from { transform: translateX(60px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.story-track section {
animation: slide-in 1s linear both;
animation-timeline: --story;
}
现在,当你水平滚动故事容器时,每个章节会随着滚动条的前进而逐步进入,进度完全同步于滚动位置。这种效果在作品集展示、产品详情页中颇具吸引力。
高级技巧:通过@keyframes分段控制
除了简单的起止动画,你还可以在@keyframes中定义多个阶段,实现更复杂的滚动联动。例如,让一个元素先旋转再平铺:
@keyframes complex-move {
0% { transform: rotate(0deg) translateX(0); }
30% { transform: rotate(180deg) translateX(100px); }
70% { transform: rotate(180deg) translateX(200px); }
100% { transform: rotate(360deg) translateX(300px); }
}
.element {
animation: complex-move 1s linear both;
animation-timeline: scroll(root);
}
不同的百分比对应滚动进度的不同阶段,这让设计师可以像编排关键帧一样编排滚动体验。
注意事项与调试方法
- 滚动容器限制:只有可滚动的元素可以作为时间线源。如果使用
scroll(root),确保页面确实有足够的可滚动内容,否则动画不会播放。 - 与
position: sticky的结合:固定定位的进度条和滚动驱动动画是天作之合,但要注意内部元素的层叠上下文。 - DevTools支持:Chrome的开发者工具已经能显示滚动驱动动画的时间线,在“Animations”面板中可以看到与滚动位置绑定的播放头,便于调试。
- 性能:这类动画运行在合成线程,不会触发重排重绘,但仍应避免动画属性含有
width或height等布置属性,推荐使用transform和opacity。
总结
滚动驱动动画把“滚动”从一种简单的视图移动,变成了可以精确编排的动画引擎。从简单的进度条到复杂的视差效果,原生CSS都能提供高性能、低代码的实现方案。虽然浏览器支持还不是100%覆盖,但作为渐进增强的一部分,它已经可以在主流浏览器上发挥实力。
下次当产品经理要求“根据滚动做一个XX效果”时,不妨先想想能不能用animation-timeline解决。很可能几行CSS就能替代上百行的滚动监听代码,而且体验还更顺滑。

