先记住这个答案
以前做滚动进度条、视差效果要监听 scroll 事件再用 JS 算比例,现在可以用纯 CSS 搞定。给元素写好 @keyframes,把 animation-timeline 设成 scroll(),动画进度就和滚动容器的滚动位置同步。不需要 JS,不会掉帧,因为浏览器在合成线程就能完成计算。
scroll()跟整个滚动容器走view()跟元素进出视口走- 必须去掉
animation-duration的时间值 - 不支持的浏览器直接无动画,不会报错
用 scroll() 做阅读进度条
最常见的场景是页面顶部的阅读进度条。用户滚动页面时,一根横条从左到右慢慢填满。
/* 进度条从宽度 0 到 100% */
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: #3b82f6;
transform-origin: left;
animation: grow linear both; /* 不写具体秒数 */
animation-timeline: scroll(root); /* 绑定根滚动容器 */
}这里有两个关键点。第一,animation-timeline: scroll(root) 把动画绑到了整个页面的滚动容器上,root 就是 <html> 的滚动容器。第二,animation 缩写里不要写具体的秒数,比如 1s,否则浏览器会把它当成普通的时间驱动动画。用 linear 是因为滚动位置本身已经是线性的,再加缓动函数反而不直观。
scroll() 还能接受 nearest(最近的可滚动祖先)、self(元素自身)这些值,以及 block 或 inline 来指定方向。
用 view() 做元素入场动画
scroll() 绑的是整个容器的滚动进度,view() 绑的是某个元素在视口里的可见程度。元素刚进入视口时是 0%,完全可见时是 100%,适合做淡入、滑入这类入场动画。
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}animation-range 控制动画在哪个阶段播放。entry 0% 是元素底边刚碰到视口底边,entry 100% 是元素完全进入视口。这样动画只在元素进入的过程中播放,滚出去时不会倒放。如果想让它滚出去时也有动画,可以加上 exit 阶段。
兼容性和降级方案
截至 2025 年底,Chrome 115+、Edge 115+ 和 Firefox 110+ 支持 animation-timeline,Safari 尚未支持。好消息是不支持的浏览器只是不执行动画,不会影响布局,所以可以做渐进增强:能跑就跑,不能跑就静态展示。
如果一定要在 Safari 上有动画效果,可以用 @supports 检测是否支持,不支持时走降级方案。
@supports 做渐进增强CSS@supports not (animation-timeline: scroll()) {
.progress-bar {
/* Safari 降级:直接显示静态条或改用 JS */
display: none;
}
}这样支持的浏览器正常显示进度条,不支持的浏览器隐藏这个元素,不影响页面功能。以前的 scroll-timeline-name 和 scroll-timeline-axis 这套命名写法在新版规范里被简化了,现在推荐直接用 scroll() 和 view() 函数,老写法有些浏览器已经废弃。
容易答错的地方
- 以为要配合 JS 的
IntersectionObserver - 滚动驱动动画是纯 CSS 方案,浏览器在合成线程处理,完全不需要 JS 参与。
IntersectionObserver适合做「进入视口时加 class」这类逻辑判断,但如果只是让动画跟滚动走,多此一举。 - 写了具体的
animation-duration秒数 - 比如
animation: fade-in 0.5s linear both,这里的0.5s会让浏览器忽略animation-timeline,退回时间驱动。正确写法是省略秒数或写auto。 - 搞混
scroll()和view()的用途 scroll()跟的是容器的整体滚动进度,适合进度条、视差背景这类全局效果。view()跟的是元素自身在视口中的位置,适合入场、退场动画。用反了虽然不报错,但动画时机和预期完全对不上。
面试官还会怎么问?
滚动驱动动画和 JS 的 scroll 事件监听相比有什么优势?
滚动驱动动画跑在合成线程,不阻塞主线程,不会因为 JS 执行慢而掉帧。scroll 事件在主线程触发,频率高时容易卡顿,还需要手动做 requestAnimationFrame 节流。纯 CSS 方案代码量也少得多。
animation-range 能精确到像素吗?
可以。animation-range 支持长度值,比如 entry 50px 表示元素底边进入视口 50px 时开始。也可以混用百分比和像素,比如 entry 0% entry 200px。
滚动驱动动画能暂停吗?
能,把 animation-play-state 设成 paused 就行。不过实际意义不大,因为用户停止滚动时动画自然就停在当前帧了。更常见的做法是用 animation-range 限定动画只在某个滚动区间内生效。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。