登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

CSS animation-timeline怎样按滚动进度驱动进度条

来源:17golang原创

时间:2026-11-05 03:35:22 346浏览 收藏

热门推荐
漫画APP
动画内容聚合,热门资源快捷查看
立即下载

如果页面内容比较长,阅读进度条不一定要依赖 JavaScript 监听 scroll 事件。CSS 的 animation-timeline 可以把动画从默认的“按时间播放”改成“按滚动进度播放”,再配合 scroll(),就能让一个元素的宽度从 0% 逐渐变成 100%。

最小做法是:给页面或内容区制造真实的滚动轴,让进度条绑定 animation-timeline: scroll(),然后用关键帧改变它的宽度。若目标环境不支持滚动驱动动画,就保留一个静态的基础样式。

先把目标拆成四个部分

这类需求最容易混淆的是“动画”和“时间线”。普通 CSS 动画默认使用文档时间线,页面加载后经过多少时间,动画就推进多少;animation-timeline 则允许把推进来源换成滚动进度或元素可见度。

本文只做阅读进度条:滚动容器提供进度,scroll() 生成匿名滚动进度时间线,@keyframes 描述宽度变化,进度条负责显示结果。view() 会在兼容边界一节对比,但不把它混入主实现。

部分职责排查重点
滚动容器产生可计算的滚动距离是否真的有溢出和滚动条
scroll()选择滚动来源和方向scroller、axis 是否选对
@keyframes把时间线进度映射成样式关键帧是否改变了目标属性
进度条把 0% 到 100% 变成可见宽度定位和尺寸是否不会遮住内容

用最小结构把滚动进度接到进度条

先看一个不依赖脚本的最小版本。示例让整个文档作为滚动来源,进度条固定在页面顶部;动画本身只负责把伪元素的 transform 从 0 缩放到 1。




一篇需要滚动阅读的文章

这里放置正文内容,内容高度应当超过视口。

实际项目可以继续放置段落、代码和图片。

/* 进度条固定在顶部,但不参与鼠标命中 */
.reading-progress {
  position: fixed;
  inset: 0 0 auto;
  height: 4px;
  background: #1b75d0;
  transform-origin: left center;
  transform: scaleX(0);
  z-index: 10;
  pointer-events: none;
}

/* 用关键帧把滚动时间线映射成从 0 到 1 的横向缩放 */
@keyframes reading-progress-width {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

/* scroll() 默认寻找最近的滚动祖先;页面滚动时也可显式使用 root */
.reading-progress {
  animation: reading-progress-width linear both;
  animation-timeline: scroll(root block);
}

这里的 linear 只是让关键帧按时间线线性推进,真正改变动画进度的是最后一行 animation-timeline。不要只写 animation 而漏掉时间线,否则进度条会按默认文档时间线播放。

滚动容器通过 scroll 时间线和关键帧驱动阅读进度条的静态结构说明图
图1:结构说明图,展示滚动容器如何通过 animation-timeline: scroll() 推进进度条关键帧。

选对 scroller 和滚动轴

scroll() 可以接滚动来源和轴向两个参数。nearest 表示最近的可滚动祖先,root 表示根元素,self 表示当前元素自身;轴向可以写 block、inline、x 或 y。

如果你要做一个局部文章容器的进度条,最好把时间线绑定到这个容器,而不是让进度条误跟随整个页面。下面把滚动区域明确放在 .reader 上,并让进度条元素位于该区域内部。


局部滚动文章

把多段正文放在这里,使 reader 的内容高度超过自身高度。

进度条会随着 reader 的 block 轴滚动而变化。

/* 让 reader 成为最近的垂直滚动祖先 */
.reader {
  position: relative;
  height: 28rem;
  overflow-y: auto;
  border: 1px solid #d8e1ec;
}

/* 局部进度条固定在 reader 顶部,避免盖住正文点击区域 */
.reader-progress {
  position: sticky;
  inset-block-start: 0;
  height: 4px;
  background: #0f8b8d;
  transform: scaleX(0);
  transform-origin: left center;
  z-index: 1;
  pointer-events: none;
  animation: reader-progress linear both;
  /* reader 是最近滚动祖先,block 表示随垂直阅读方向推进 */
  animation-timeline: scroll(nearest block);
}

/* 关键帧只描述显示属性,不负责寻找滚动容器 */
@keyframes reader-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

如果容器使用横向滚动,改成 scroll(nearest inline) 或 scroll(x)。如果指定的轴没有滚动条,时间线会处于不活动状态,动画看起来就像完全没有生效。因此,调试时先确认 overflow、容器尺寸和内容溢出,再检查关键帧。

补上兼容降级和动画偏好

滚动驱动动画属于浏览器平台能力,不能假定所有访问者都支持。最实用的降级方式是先给进度条一个不依赖动画的基础外观,再用 @supports 在支持时启用时间线。这样即使动画规则被忽略,页面也不会出现一条完全不可见的装饰层。

/* 基础回退:不支持滚动时间线时仍然保留清晰的顶部标记 */
.reading-progress {
  width: 100%;
  background: #d7e8f6;
}

/* 只有浏览器理解 scroll() 时才启用动态宽度映射 */
@supports (animation-timeline: scroll()) {
  .reading-progress {
    width: auto;
    animation: reading-progress-width linear both;
    animation-timeline: scroll(root block);
  }
}

/* 尊重用户的减少动态效果偏好,保留静态提示而不强制播放动画 */
@media (prefers-reduced-motion: reduce) {
  .reading-progress {
    animation: none;
    transform: scaleX(1);
  }
}

view() 与 scroll() 的关注点不同:前者根据目标元素在滚动视口中的可见程度推进,适合做卡片进入动画;后者根据滚动容器已经滚过的距离推进,更适合本文的阅读进度条。两者都依赖明确的滚动上下文,不能用一个没有溢出的普通容器代替。

scroll view 和 supports 静态回退边界的 CSS 时间线对比说明图
图2:边界说明图,对比滚动进度时间线、可见度时间线和不支持时的静态降级路径。

按固定顺序排查不生效

遇到进度条停在原地时,建议按下面的顺序检查,通常不需要立刻改成 JavaScript:

  1. 确认有滚动距离。检查目标容器是否设置了合适的高度或宽度,内容是否真的溢出;没有滚动条就没有可推进的 scroll() 时间线。
  2. 确认轴向一致。垂直阅读一般用 block 或 y,横向列表用 inline 或 x。书写模式变化时,逻辑轴比固定物理轴更稳妥。
  3. 确认关键帧改变了目标属性。如果关键帧写的是 width,目标元素不能又被固定宽度覆盖;使用 transform: scaleX() 时要保留正确的 transform-origin。
  4. 确认简写顺序。animation 简写可能重置 animation-timeline。先写动画简写,再写 animation-timeline,不要在后面再次用简写覆盖它。
  5. 确认降级规则没有隐藏结果。检查 @supports 条件、层叠顺序和 prefers-reduced-motion 是否把动态规则改成了静态状态。
/* 用支持条件做最小排查,不在不支持的环境中强行覆盖样式 */
@supports (animation-timeline: scroll()) {
  .reading-progress {
    /* 先设置动画参数,再在下一行指定时间线,避免被简写重置 */
    animation: reading-progress-width linear both;
    animation-timeline: scroll(root block);
  }
}

/* 不支持时保留静态底色,帮助用户识别页面顶部的阅读提示 */
@supports not (animation-timeline: scroll()) {
  .reading-progress {
    transform: scaleX(1);
  }
}

最后记住这张速查表

需求写法注意点
跟随页面垂直滚动scroll(root block)根页面必须有可滚动内容
跟随最近局部容器scroll(nearest block)最近祖先要有滚动条
跟随横向滚动scroll(nearest inline)确认 inline 轴确实溢出
浏览器不支持@supports not (...)保留静态颜色或静态宽度
用户减少动态效果prefers-reduced-motion: reduce关闭动画并保留可理解的状态

核心顺序可以压缩成一句话:先让容器产生真实滚动,再选择 scroll() 的来源和轴,接着用关键帧描述视觉变化,最后用 @supports 和减少动态效果媒体查询补上边界。这样一个阅读进度条就能保持纯 CSS,同时在不支持新时间线的环境中仍有可见的基础反馈。

常见追问

为什么 animation-timeline 写了却没有动画?

优先检查目标滚动轴是否存在滚动条,再检查 animation 简写是否在后面重置了时间线,以及关键帧是否真的改变了目标元素。

scroll() 和 view() 能互相替代吗?

不能完全替代。scroll() 表达滚动容器的整体进度,view() 表达某个元素进入和离开视口的可见度进度,适用的阅读模型不同。

一定要把进度条放进滚动容器吗?

不一定,但要明确时间线寻找的滚动祖先。放在局部容器内时更容易使用 scroll(nearest ...);放在页面外层时则要显式考虑根文档滚动。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>