一篇有滚动进度的文章
这里放足够长的正文,让滚动范围真实存在。
如果页面上有阅读进度条、滚动时逐渐出现的卡片,很多旧实现都会在 scroll 事件里读取滚动距离,再手动算出百分比并写回样式。CSS scroll-driven animations 提供了更直接的做法:让 @keyframes 的进度由滚动时间线控制,而不是由时间流逝控制。这样动画的当前位置天然对应滚动位置,向上滚动时也会反向回退。
这篇文章只解决一个问题:如何把 CSS 动画绑定到滚动进度。你会看到 scroll()、view() 和命名时间线的区别,也会知道为什么 animation-timeline 要写在 animation 简写之后。
假设要做一个页面顶部进度条,传统写法需要监听滚动事件、取得文档高度、计算当前百分比,再把结果写进 CSS 变量。它能工作,但业务代码需要自己处理滚动容器、节流、边界值和主线程上的样式更新。
const progress = document.querySelector('.reading-progress');
function updateProgress() {
// 计算页面可滚动的总距离,避免把视口高度重复算入分母。
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const ratio = maxScroll > 0 ? window.scrollY / maxScroll : 0;
// 将范围限制在 0 到 1,防止橡皮筋滚动产生异常值。
progress.style.transform = `scaleX(${Math.min(1, Math.max(0, ratio))})`;
}
window.addEventListener('scroll', updateProgress, { passive: true });
updateProgress();
问题不在于这段代码一定错误,而在于它把“滚动位置如何驱动动画”变成了 JavaScript 维护的状态。CSS 时间线可以把这层关系交给样式系统,让脚本只负责真正的业务交互。
普通 CSS 动画默认使用基于时间的文档时间线,animation-duration 决定它在多少秒内完成。滚动驱动动画改用滚动进度时间线或视图进度时间线:滚动条走到哪里,关键帧就推进到哪里;滚动暂停,动画也停在当前进度。
这里要区分两个相近概念:
本文讨论第一种。选错概念时,常见结果是动画只触发一次,却没有和滚动位置保持连续对应。

最小写法是给元素声明一个关键帧动画,再使用 animation-timeline: scroll()。scroll() 可以接收滚动容器和轴向参数,例如 scroll(nearest block) 表示沿最近的块轴滚动容器取进度。
一篇有滚动进度的文章
这里放足够长的正文,让滚动范围真实存在。
.reader-shell {
/* 让示例拥有独立的纵向滚动容器。 */
height: 28rem;
overflow-y: auto;
}
.reading-progress {
/* 缩放原点放在左侧,进度从左向右增长。 */
position: sticky;
inset-block-start: 0;
z-index: 1;
height: 0.35rem;
background: #2563eb;
transform-origin: left center;
transform: scaleX(0);
animation: fill-progress linear both;
/* timeline 写在 animation 简写之后,避免被简写重置。 */
animation-timeline: scroll(nearest block);
}
@keyframes fill-progress {
/* 关键帧只描述视觉变化,进度交给滚动时间线。 */
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
这段代码里没有设置秒数,因为动画进度不再由秒数推动。linear 保持滚动百分比和关键帧百分比的对应关系,both 让元素在时间线之外也保留关键帧的起止样式。
当同一个滚动容器需要控制多个动画时,可以先给容器命名时间线,再让后代元素通过 animation-timeline 引用它。命名方式适合组件化场景,因为“谁提供进度”和“谁消费进度”会分开表达。
.chapter-list {
/* 暴露一个可被后代引用的纵向滚动时间线。 */
scroll-timeline-name: --chapter-scroll;
scroll-timeline-axis: block;
overflow-y: auto;
height: 24rem;
}
.chapter-list .chapter-marker {
/* 这个元素消费容器提供的命名时间线。 */
animation: marker-shift linear both;
animation-timeline: --chapter-scroll;
}
@keyframes marker-shift {
/* 标记随章节滚动从透明移动到可见位置。 */
from {
opacity: 0.25;
transform: translateX(-0.75rem);
}
to {
opacity: 1;
transform: translateX(0);
}
}
命名时间线的关键是作用域。滚动容器必须真的能滚动,动画元素必须位于能看到这条时间线的关系范围内;如果容器没有溢出内容,时间线就没有可推进的有效范围。
scroll() 关注的是滚动容器从起点到终点的整体进度;view() 关注的是某个元素在滚动视口中的可见进度。做“卡片进入视口时逐渐显现”“章节标题经过视口中线时完成强调”时,view() 更符合问题本身。

.feature-card {
/* animation 简写先声明动画,再指定由元素可见范围驱动。 */
animation: reveal-card linear both;
animation-timeline: view(block);
/* 缩小有效范围,让卡片靠近视口中央时才进入主要动画段。 */
animation-range: entry 15% cover 45%;
}
@keyframes reveal-card {
/* 卡片进入可见范围时,从轻微位移过渡到正常状态。 */
from {
opacity: 0;
transform: translateY(1.25rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
animation-range 控制关键帧映射到时间线的哪一段。它不是给动画加一个延迟计时器,而是改变元素进入、覆盖视口等范围与关键帧之间的对应关系。向上滚动时,元素离开视口,动画也会反向回退。
| 现象 | 优先检查 | 处理方式 |
|---|---|---|
| 动画像普通时间动画一样自动播放 | 是否漏写 animation-timeline | 在 animation 简写之后补上 scroll()、view() 或命名时间线 |
| 滚动但完全没有进度 | 滚动容器是否有 overflow 和实际溢出内容 | 确认 scrollHeight 大于容器可视高度 |
| 命名时间线找不到 | 名称和作用域是否一致 | 核对 --chapter-scroll 拼写、容器层级和轴向设置 |
| 卡片只在第一次出现时动一下 | 是否误用了 scroll-triggered 思路 | 改用 view() 时间线,并按可见范围映射关键帧 |
| 简写后 timeline 没效果 | 声明顺序 | 把 animation-timeline 放到 animation 之后 |
MDN 当前将 animation-timeline、scroll() 等能力标记为 Limited availability,说明它们还不是所有主流浏览器都一致支持。生产页面不要把关键内容的可见性完全押在这组动画上:内容默认应该可读,动画只是增强。
可以先用静态样式保证布局,再通过特性查询启用滚动动画,并尊重用户的减少动态效果偏好:
.feature-card {
/* 不支持滚动时间线时,卡片仍然保持可读的默认状态。 */
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
.feature-card {
/* 只有浏览器支持且用户未要求减少动态效果时才增强。 */
animation: reveal-card linear both;
animation-timeline: view(block);
animation-range: entry 15% cover 45%;
}
}
}
如果业务必须支持不具备该能力的环境,再补一个轻量 JavaScript 回退即可;回退应只负责改变一个 CSS 类或变量,不要和 CSS 主路径同时争夺同一个动画属性。
先判断你要表达的是“整个容器滚了多少”,还是“某个元素进入视口多少”。前者优先选择 scroll() 或命名 scroll-timeline,后者优先选择 view()。然后按“默认可读、动画可选、减少动态效果优先”的顺序组织样式,最后在目标浏览器矩阵中实际确认支持情况。
CSS scroll-driven animations 的价值不是把所有滚动交互都改成 CSS,而是让连续的视觉进度回到声明式时间线中。这样代码更接近设计意图,也更容易把滚动容器、可见范围和关键帧之间的关系讲清楚。
可以。它们可以服务于同一页面中的不同元素:整体阅读进度适合 scroll(),卡片进入视口的显现适合 view()。不要让一个元素同时依赖两条互相冲突的时间线。
因为动画进度由滚动时间线推进,而不是由经过几秒推进。应该把注意力放在时间线来源和 animation-range 上。
只要默认样式保持可读就不会。不要用只在关键帧末端才出现的状态承载正文信息,把动画当作渐进增强。
cgroup v2 io.weight 调整块设备相对优先级