上个月在一个产品介绍页里,产品经理要求加上滚动时的进度条、首屏标题随滚动淡入、还有几组视差背景。按照以前的做法,我得先监听scroll事件,计算滚动偏移量,再用requestAnimationFrame更新元素的样式或CSS变量。代码写了一堆,还得小心性能,在低端设备上滚动往往会掉帧。
后来把整套逻辑迁移到了CSS滚动驱动动画上——进度条只需两行声明,视差效果交给了scroll-timeline,元素入场动画用view-timeline搞定。整个产品页的JavaScript滚动监听几乎被删光了,滚动帧率反而稳在了60fps。这篇文章就把这些实操技巧完整端出来,看完你也能在项目里立刻上手。
核心概念:时间线从滚动容器来
CSS动画通常依赖时间轴——默认是钟表时间(animation-duration和animation-delay)。而滚动驱动动画让动画的进度由滚动容器的滚动偏移量(或元素在视口中的可见性)来决定。
主要有两种函数定义这种时间线:
scroll()—— 基于滚动容器的滚动进度,从0%滚到100%对应动画的0%到100%。view()—— 基于元素相对于视口的出现与消失,比如元素开始进入视口到完全离开视口,控制动画的进行阶段。
两者都需要在animation-timeline属性中指定,取代传统的animation-duration作为进度来源。同时,你仍然可以使用@keyframes定义动画帧。
案例一:全站滚动进度条
很多阅读型网页顶部会有一条细线,随滚动不断增长,直观显示当前文章读到哪了。用CSS实现,甚至不需要任何JavaScript。
先写一个HTML结构,放置进度条容器:
<body>
<div class="progress-bar"></div>
<!-- 页面其他内容 -->
</body>
CSS核心代码:
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #4facfe, #00f2fe);
/* 指定动画由页面滚动驱动,沿着纵向滚动 */
animation: grow-progress linear;
animation-timeline: scroll(root);
transform-origin: left center;
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
这里 animation-timeline: scroll(root) 表示以根元素(html)的滚动进度作为时间线。滚动条从顶部滚到底部,动画自动从0%走到100%,进度条也就从左到右填满整屏。不需要监听任何事件,也无需手动计算百分比。
如果希望进度条用在某个特定的滚动容器(比如一个带滚动条的区域),可以指定scroll(nearest) 或 scroll(self),甚至可以自定义一个具名滚动时间线。
案例二:视差滚动背景
以前做视差效果,要么用translateY配合JavaScript,要么用多个background-attachment: fixed去模拟。现在可以利用滚动进度直接驱动背景偏移。
设想一个场景:首屏大图背景,随着用户向下滚动,背景缓慢上移,形成深度感。
<header class="hero">
<h1>发现自然之美</h1>
</header>
<section>...长内容...</section>
CSS如下:
.hero {
height: 100vh;
background-image: url('mountain.jpg');
background-size: cover;
background-position: center;
animation: parallax-move linear;
animation-timeline: scroll(root);
}
@keyframes parallax-move {
from { background-position-y: 0%; }
to { background-position-y: 40%; } /* 背景以不同速度移动 */
}
滚动页面时,background-position-y 从0%变化到40%,产生视差效果。因为动画由根滚动驱动,不会有任何JS干预,而且浏览器可以将此效果提升到合层线程,即使主线程忙着加载图片,滚动依然顺滑。
案例三:元素进入视口时淡入上移(view-timeline)
很多营销页面喜欢让卡片、标题在滚动到可见区域时才出现并上浮。这曾是各种“scroll animation”库的主战场,现在CSS的view()函数可以单挑。
先准备一组卡片:
<div class="card-container">
<div class="card">...</div>
<div class="card">...</div>
<div class="card">...</div>
</div>
为了给每张卡片添加进出场动画,我们使用view-timeline:
.card {
opacity: 0;
transform: translateY(40px);
animation: fade-in-up linear;
/* 以自身在视口中的可见性作为时间线,覆盖从进入10%到离开90%的过程 */
animation-timeline: view(block 10% 90%);
animation-range: entry 0% cover 30%; /* 可进一步精确控制动画范围 */
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
这里 view(block 10% 90%) 定义了时间线的开始点为元素刚进入视口底部10%(即元素顶部距离视口底部10%位置时),结束点为元素离开视口90%(元素底部距离视口顶部90%位置时)。animation-range 则进一步限定了动画发生的区间:从元素进入视口(entry)到覆盖30%视口高度的位置,动画就播放完毕,这样卡片在完全进入视口前就已经淡入到位,看起来非常自然。
当页面滚动时,每张卡片会根据自身的可见性独立播放动画,完全不用一行JS。
几个容易踩到的细节
- 滚动容器必须可滚动:如果使用
scroll(nearest)但祖先容器没有滚动条,动画将没有进度。确保指定了正确的滚动源。 - 与正常动画叠加:如果同时设置了
animation-duration,animation-timeline会覆盖它,但你可以使用animation-timeline结合animation-range来细化起止位置,而不需要duration。 - 浏览器支持:Chrome 115+、Edge 115+、Safari 17.4+、Firefox 130+(需开启
layout.css.scroll-driven-animations.enabled)。生产环境建议使用@supports (animation-timeline: scroll())进行检测,并提供降级方案。 - 性能:由于动画绑定在合成属性(如
transform和opacity)上,浏览器可以在合成线程上运行,不需要回流或重绘。尽量避免在滚动动画里使用width、margin等布局属性。
比JavaScript更好维护的原因
过去用scroll事件驱动动画,往往需要计算各种高度、判断元素可见性,代码散布在多个地方。CSS滚动驱动动画将动画的定义与实际滚动行为绑定于一处,便于整体管理。样式与行为都在CSS内,视图与控制完全分离,而且滚动动画天然支持“反向”播放——当你往回滚动,动画会倒带回退,这点在JS里做起来很繁琐,而CSS自动实现。
总结
滚动驱动动画给CSS赋予了更贴合人眼感知的表达能力。进度条、视差、入场动画,这些原本需要脚本护持的动态效果,现在可以像写静态样式一样轻松组织。而且因为省去了监听与计算,移动端的滚动手感往往比JS方案更顺畅。
如果你正在做一个偏向内容展示的产品页面,不妨把这几个案例拿去试,从删除那一大堆scroll事件监听开始,体会纯粹由样式驱动的滚动交互——少即是多。

