一篇足够长的文章
这里放置能够产生纵向溢出的正文……
scroll-timeline 的核心作用,是把滚动容器从起点到终点的位置映射成一条 0%~100% 的动画时间轴。把这条时间轴交给 animation-timeline 后,普通 @keyframes 就不再按经过的秒数推进,而是随用户向前或向后滚动而前进或回退。
最稳妥的入门方式是:在真正可滚动的容器上声明一个以 -- 开头的时间轴名称,再让进度条引用同一个名称。当前 MDN 仍把该能力标为 Limited availability,所以生产页面应把它当作渐进增强,而不是唯一可用路径。
官方参考:https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-timeline
标准草案:https://drafts.csswg.org/scroll-animations-1/
下面的结构把文章内容放进一个固定高度、可纵向滚动的容器。进度条固定在容器顶部,初始宽度为完整宽度,再通过 scaleX() 从 0 缩放到 1。
一篇足够长的文章
这里放置能够产生纵向溢出的正文……
.article-shell {
block-size: 70vh;
overflow-y: auto; /* 必须真的可滚动,时间轴才会激活 */
scroll-timeline: --reading block; /* 名称在前,滚动轴在后 */
}
.progress {
position: sticky;
inset-block-start: 0;
inline-size: 100%;
block-size: 4px;
background: #2563eb;
transform: scaleX(0);
transform-origin: left center; /* 从左侧开始增长 */
animation: grow 1ms linear both;
animation-timeline: --reading; /* 必须写在 animation 简写之后 */
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
scroll-timeline 是 scroll-timeline-name 与 scroll-timeline-axis 的简写。名称必须是以 -- 开头的 dashed-ident;省略轴时默认为 block,它会随书写模式对应到块方向。这里明确写出 block,是为了让代码意图更直观。
animation-duration: 1ms 不代表动画只运行 1 毫秒。滚动时间轴接管后,真正决定进度的是滚动位置;保留一个极短 duration 主要是兼顾部分实现和旧示例习惯。更关键的规则是 animation-timeline 要放在 animation 简写之后,否则后出现的简写可能把时间轴重置为默认值。

滚动进度时间轴关心的是“已经滚了多少”和“总共还能滚多少”的比例。以普通纵向容器为例,可以把它理解成下面这组关系:
可滚动范围 = scrollHeight - clientHeight 当前进度 = scrollTop / 可滚动范围 顶部位置 -> 0% 中间位置 -> 约 50% 底部位置 -> 100%
这段是概念说明,不需要在 JavaScript 中手动计算。浏览器会把滚动范围映射到时间轴,再用结果采样 @keyframes。用户向上滚时,动画同样会反向回退;停止滚动时,进度保持在当前位置。
如果容器没有溢出,可滚动范围为零,时间轴就无法产生有效进度。因此“CSS 写对了但动画不动”时,首先检查的不是关键帧,而是滚动源是否真的存在滚动条,以及内容高度是否超过容器。
只有一个动画目标,而且它明确由最近的祖先滚动容器控制时,可以省略命名过程,直接使用匿名时间轴 scroll():
.card-progress {
animation: grow 1ms linear both; /* 先声明关键帧动画 */
animation-timeline: scroll(nearest block); /* 最近祖先的块轴滚动条 */
}
.page-progress {
animation: grow 1ms linear both;
animation-timeline: scroll(root block); /* 整个页面的根滚动区 */
}
scroll() 的滚动源可选 nearest、root 或 self,轴可选 block、inline、x 或 y。默认值是 nearest block。在横向轮播中,应明确写成 scroll(nearest inline) 或按实际书写模式选择 x。
多个元素需要共享同一个时间轴、滚动容器与动画目标距离较远,或者希望名称能表达业务语义时,命名 scroll-timeline 更易维护。只有一个局部效果时,匿名 scroll() 更短。两者都在做同一件事:选择滚动源与滚动轴,再把结果交给动画。
| 现象 | 常见原因 | 修正方式 |
|---|---|---|
| 完全不动 | 容器没有溢出,或 overflow 为 hidden/clip | 给容器明确尺寸并保证内容超出 |
| 仍按时间播放 | animation 简写写在 animation-timeline 后面 | 把 animation-timeline 放到简写之后 |
| 引用不到时间轴 | 名称没有以 -- 开头,或声明与引用不一致 | 统一使用例如 --reading 的名称 |
| 方向不对 | 把 block/inline 与 x/y 混为一谈 | 根据书写模式与实际滚动方向选择轴 |
| 部分浏览器无效果 | 特性尚未达到 Baseline 广泛可用 | 用 @supports 保留静态或 JS 降级 |
还有一个容易忽略的点:scroll-timeline 应声明在提供滚动位置的元素上,而 animation-timeline 应声明在真正执行关键帧的元素上。把两者全部写在进度条本身,除非进度条恰好也是滚动容器,否则往往选错滚动源。
当前浏览器支持并不完全一致,最简单的策略是让基础页面在没有滚动动画时仍然可读,再只对支持 animation-timeline 的环境启用进度映射。
.progress {
inline-size: 100%;
transform: none; /* 默认保持静态可见,不依赖新特性 */
}
@supports (animation-timeline: scroll()) {
.progress {
transform: scaleX(0);
transform-origin: left center;
animation: grow 1ms linear both; /* 简写先出现 */
animation-timeline: scroll(root block); /* 时间轴后出现 */
}
}
@media (prefers-reduced-motion: reduce) {
.progress {
animation: none; /* 尊重减少动态效果的系统偏好 */
transform: scaleX(1);
}
}
如果阅读进度是业务必需信息,而不仅是装饰,可以再提供一个 JavaScript 降级:监听滚动并更新 CSS 自定义属性。但要避免在每个 scroll 事件里触发布局抖动,最好缓存容器尺寸、使用 requestAnimationFrame 合并更新,并在内容尺寸改变时重新计算。

如果目标是整页阅读进度条,匿名根滚动时间轴最简洁。下面把结构、关键帧、特性检测和减少动态效果放在一起:
文章标题
正文内容……
.reading-progress {
position: fixed;
inset: 0 0 auto 0;
z-index: 10;
block-size: 4px;
background: #2563eb;
transform-origin: left center;
}
@keyframes reading-grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
@supports (animation-timeline: scroll()) {
.reading-progress {
animation: reading-grow 1ms linear both; /* 设置关键帧与填充状态 */
animation-timeline: scroll(root block); /* 用整页纵向滚动驱动 */
}
}
@media (prefers-reduced-motion: reduce) {
.reading-progress {
animation: none; /* 减少动态效果时保留静态色条 */
transform: scaleX(1);
}
}
这个版本没有滚动事件监听,也不需要手动计算百分比。对支持滚动驱动动画的浏览器,它会随页面滚动平滑增长和回退;对不支持的环境,它仍是一条普通的静态提示条。若静态满格容易误导,可以在默认规则中改为隐藏,并在 @supports 内恢复显示。
scroll-timeline 关注滚动容器自身从起点到终点的滚动比例,适合阅读进度、轮播进度和整体视差。view-timeline 关注某个元素进入、穿过、离开滚动视口的可见过程,更适合元素入场动画。
因为滚动时间轴用位置而不是经过时间驱动关键帧。duration 不再是“播放多久”的主要控制项。要改变视觉节奏,可以调整关键帧分布、缓动函数,或者换用更合适的滚动范围。
inline 会跟随书写模式,适合国际化布局;x 始终指物理水平方向。组件跟随书写模式时优先使用 inline,明确固定为水平坐标轴时再使用 x。
纯视觉映射通常可以,但业务状态、复杂跨组件同步、旧浏览器支持和精确埋点仍可能需要 JavaScript。建议先把视觉效果做成渐进增强,再判断是否值得引入脚本降级。
scroll-timeline 最值得记住的不是某一段固定代码,而是三层关系:滚动容器提供时间轴,动画目标引用时间轴,关键帧描述 0% 到 100% 的视觉变化。只要滚动源、轴和声明顺序正确,阅读进度条这类效果就能用很少的 CSS 完成。
photocolors色盘模式和记忆卡片模式怎么选?功能差异与使用场景