过去想在页面上做一个跟随滚动的进度条,或者某个元素随着滚动淡入淡出,基本离不开 window.addEventListener('scroll', ...)。监听滚动事件、计算滚动比例、手动更新样式,这套流程写了无数遍。但滚动事件触发频率很高,即使加了节流,在低性能设备上还是可能掉帧,而且逻辑散落在 JavaScript 里,和样式分离,维护起来总差口气。
从 2023 年开始,浏览器开始陆续支持 CSS 滚动驱动动画,这是一套全新的规范,让动画的进度直接由滚动位置来控制。核心思想很简单:定义一个时间线(timeline),这个时间线的“时间”由滚动偏移量决定,然后把这个时间线附加到 CSS 动画上。滚动多少,动画就播放多少,完全在合成器线程上运行,性能远超 JavaScript 方案。本文会用三个实际案例——页面顶部进度条、滚动视差效果、元素进入视口时的淡入动画——把这个新特性从头到尾讲清楚。
核心概念:ScrollTimeline 与 ViewTimeline
传统 CSS 动画是在真实的时间轴上运行的,比如 animation-duration: 2s 表示两秒播完。滚动驱动动画引入了两种新的时间线来源:
- ScrollTimeline:基于某个滚动容器的滚动偏移量。滚动条从顶部滚到底部,时间线走完一个完整周期。
- ViewTimeline:基于某个元素相对于视口的可见性。元素从开始进入视口到完全离开视口,时间线走完一个周期。
将这些时间线绑定到 animation-timeline 属性上,动画就不再由 animation-duration 控制时长,而是由滚动位置决定。浏览器会在合成阶段计算这些动画,不占用主线程,意味着即使页面有大量 JavaScript 正在执行,滚动动画依然保持 60fps。
案例一:顶部滚动进度条
最常见的需求:页面顶部有一个细长的进度条,表示阅读进度。以前的做法是计算 scrollTop / (scrollHeight - clientHeight),然后用 JavaScript 更新一个 width 样式。现在用 CSS 滚动驱动动画,只需要一个固定定位的元素和一个关键帧。
先在 HTML 里放一个占位用的进度条:
<div class="progress-bar"></div>
接着是 CSS,核心在于 animation-timeline: scroll()。这个 scroll() 函数默认指向整个文档的滚动条,也可以指定为最近的滚动容器:
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #ff6b6b, #feca57);
transform-origin: left center;
animation: grow-progress linear;
animation-timeline: scroll(root);
/* 如果页面滚动容器是 body,可以使用 scroll(root block) */
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
这里用 transform: scaleX() 代替改变 width,因为 transform 动画不触发重排,帧率更稳定。滚动到页面底部时,进度条正好从 0 伸展到 100%。整个过程没有一行 JavaScript。
如果滚动容器不是整个文档,而是一个设置了 overflow-y: auto 的侧边栏,可以把 scroll(root) 换成 scroll(nearest),它会选取最近的滚动祖先。这样一来,一个组件级别的滚动进度也能轻松实现。
案例二:滚动视差效果
视差滚动让背景图片或文案移动得比页面内容慢,制造层次感。以前靠监听滚动位置然后修改 background-position 或者 translateY,现在直接让动画时间线绑定到滚动上。
假设有一个首屏大图,我们希望网页向下滚动时,图片向上移动一段距离,营造出背景缓缓移开的感觉。HTML 结构很简单:
<section class="hero">
<h1>探索自然风光</h1>
</section>
CSS 部分,用 animation-timeline: scroll() 配合一个简单的平移动画:
.hero {
height: 80vh;
background: url('hero.jpg') center/cover no-repeat;
display: flex;
align-items: center;
justify-content: center;
animation: parallax linear both;
animation-timeline: scroll(root);
/* 动画绑定在整个页面的滚动上 */
}
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-20%); }
}
当用户从页面顶部滚到底部,背景图片会逐渐向上平移 20% 的高度。由于动画是线性的,且时间线与滚动进度同步,图片移动的速度与滚动速度保持一致,不会出现 JavaScript 方案中常见的“跳帧”现象。
我们还可以利用 ViewTimeline 实现更精确的视差——一个元素在进入和离开视口的过程中播放视差动画。比如卡片从屏幕底部滑入时,内部的装饰图案逐渐放大。CSS 中用 view() 函数创建基于元素可见性的时间线:
.card {
animation: card-reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
@keyframes card-reveal {
from { opacity: 0; transform: translateY(30px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
view() 函数创建的时间线以元素进入视口(entry)到离开视口(exit)为周期。animation-range 让我们截取这个周期中的一段,比如上例表示元素从刚进入视口(0% entry)到被滚动覆盖掉 40% 时,动画完成。这意味着卡片不会等到完全离开屏幕才结束动画,而是在稍微往上滚一点时就完成了淡入上移,视觉上更灵敏。
案例三:元素入场特效,真正零 JS 的懒加载动画
很多网站都有“滚动到某个区域才出现的动画”,以前需要借助 IntersectionObserver 或者滚动事件来判断元素是否进入视口。现在可以用 ViewTimeline 和 animation-range 直接搞定,连判断逻辑都省了。
下面是一个常见的信息模块,包含图片和文字,希望在滚动到该模块约 20% 时渐显,并且图片从左边滑入、文字从右边滑入:
<div class="feature">
<img class="feature-img" src="photo.jpg" alt="">
<div class="feature-text">
<h2>极速处理</h2>
<p>采用分布式架构,毫秒级响应。</p>
</div>
</div>
对应的 CSS 用 view() 时间线,并设置不同的 animation-range 控制动画触发点:
.feature-img {
animation: slide-in-left linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
.feature-text {
animation: slide-in-right linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes slide-in-left {
from { opacity: 0; transform: translateX(-40px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(40px); }
to { opacity: 1; transform: translateX(0); }
}
这里 animation-range: entry 0% entry 100% 表示动画在元素从刚接触视口底部(entry 0%)到完全进入视口(entry 100%)之间完成。往回滚动的时候,动画会反向播放,元素又会滑出视口——这是滚动驱动动画天然支持的反向播放特性,不需要额外处理。
这种写法对性能极其友好:所有的动画计算都在合成器线程中完成,即使主线程被 JavaScript 卡住,滚动依然流畅,动画不会出现卡顿。而且省去了 IntersectionObserver 的内存开销和回调逻辑,代码量减少了至少一半。
兼容性与渐进增强
截至 2024 年底,Chrome、Edge 和 Opera 已经完整支持 scroll() 和 view() 函数,以及 animation-timeline 属性。Firefox 在 123 版本后开始支持,Safari 在 17.4 后提供部分支持。整体覆盖率还在快速提升中。
对于尚不支持的浏览器,我们可以用 @supports 来做渐进增强:
.progress-bar {
/* 老旧浏览器的静态样式 */
width: 0%;
background: #ccc;
}
@supports (animation-timeline: scroll()) {
.progress-bar {
width: auto;
background: linear-gradient(90deg, #ff6b6b, #feca57);
animation: grow-progress linear;
animation-timeline: scroll(root);
transform-origin: left center;
}
}
对于不支持滚动驱动动画的环境,进度条可以保持静态或干脆隐藏,不影响页面核心功能。视差和入场特效同理——不支持时元素就静态显示,不影响信息传达。这种策略比强行用 JavaScript 模拟更轻量,也更容易在未来删除回退代码。
小结
CSS 滚动驱动动画把滚动交互从 JavaScript 的主战场搬到了样式层。进度条、视差效果、滚动入场动画这些以往需要监听事件、计算数值、手动更新样式的操作,现在用几个 @keyframes 和 animation-timeline 就能实现,而且内建反向播放、合成器线程渲染,流畅度直接拉满。
这三个案例覆盖了 ScrollTimeline 和 ViewTimeline 的核心用法,从基于页面滚动的进度条,到基于元素可见性的入场动画,任意组合还可以做出更复杂的效果。如果你的项目现在还在用 scroll 事件驱动动画,不妨把这些效果挑一两个用新方案重写,感受一下样式层接管交互逻辑后的清爽与高效。

