CSS滚动驱动动画实战:用animation-timeline做阅读进度条与卡片揭示

2026-09-23 0 235

做滚动相关的视觉效果,过去十年基本只有一条路:监听 scroll 事件,在回调里算进度、改样式。这条路能跑,但跑得不舒服。滚动是高频事件,回调在主线程;很多代码为了保证拿到的位置是最新的,会在回调里读一次 getBoundingClientRect(),紧接着又写样式,一读一写就把浏览器逼着做强制同步布局。就算套上 requestAnimationFrame 做节流,逻辑和渲染还是挤在同一帧里抢时间。

滚动驱动动画(Scroll-driven Animations)换了个思路:把「动画的进度从哪里来」这件事交给 CSS 引擎。进度在合成阶段计算,动画走合成线程,主线程再忙它也能照常跑。这篇文章不讲概念空转,直接上四个能用的例子,然后把几个一定会踩的坑说清楚。

一、本质:把「时间轴」换掉了

普通的 CSS 动画,进度来源是时间。写 animation: fade 1s linear,浏览器就把这一秒切成若干份,按 linear 映射到 0%~100%。

滚动驱动动画做的事情是:把这条时间轴换成滚动位置。同样的关键帧,同样的缓动,只是驱动它的不再是墙上时钟,而是用户手指的滑动距离。

/* 传统写法:1 秒跑完 */
.thing {
  animation: grow 1s linear infinite;
}

/* 滚动驱动写法:滚动距离跑完 */
.thing {
  animation: grow linear;
  animation-timeline: scroll(root block);
}

注意第二段里的 animation-duration 直接消失了。这不是省略,而是写了也没用——滚动时间轴接管之后,动画时长由滚动量决定,浏览器会忽略时长声明。想调节奏,只能去改 animation-range 或者重排关键帧的百分比分布。

CSS 提供两种时间轴函数,用途完全不同:

  • scroll():盯着滚动容器本身的滚动进度。滚到顶是 0%,滚到底是 100%。适合进度条、随页面收缩的页头。
  • view():盯着某个元素在视口里的可见过程。元素刚开始进入视口是 0%,完全离开视口是 100%。适合卡片揭示、视差。

二、第一个例子:顶部阅读进度条

结构极其简单,页面最后放一个空盒子就行:

<div class="read-progress" aria-hidden="true"></div>
.read-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, #ff5f6d, #ffc371);
  transform-origin: 0 50%;
  scale: 0 1;
  animation: read-progress-grow linear;
  animation-timeline: scroll(root block);
}

@keyframes read-progress-grow {
  from { scale: 0 1; }
  to   { scale: 1 1; }
}

几个细节值得说:

一,这里没有用 transform: scaleX(),而是用了独立的 scale 属性。独立变换属性和 transform 互不干扰,以后想再加别的位移时不用担心覆盖。它同样受 transform-origin 控制,所以从左向右展开的效果照旧。

二,横向压缩会把背景渐变一起压扁,渐变颜色会跟着挤。这里用纯色或者整体色域平滑的渐变,基本看不出来;如果要放具体图案,老老实实用宽度动画或者裁剪方案。

三,animation-fill-mode 可以不写。因为整个滚动区间都落在动画范围内,不存在「范围之外要保持什么状态」的问题。

四,scroll(root block) 里的 root 指的是根滚动容器。这条最容易出错,后面单独讲。

三、第二个例子:卡片进入视口时揭示

过去这个需求基本是 IntersectionObserver 加一个 .is-visible 类,进入视口就加类,CSS 过渡负责表现。它有个天然的短板:只有「进入」和「离开」两个状态,没有中间进度。想要卡片随滚动逐渐清晰、逐渐归位,只能靠过渡曲线糊过去。

view() 直接给你一整段连续进度:

.card {
  animation: card-reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

@keyframes card-reveal {
  from {
    opacity: 0;
    translate: 0 2.5rem;
    scale: 0.95;
  }
  to {
    opacity: 1;
    translate: 0 0;
    scale: 1;
  }
}

animation-range 是这套东西里最有价值的开关。它把整条时间轴切成若干区间,常用的关键字有四个:

  • entry:元素从「开始进入视口」到「完全进入视口」这一段。
  • contain:元素完全在视口内的那一段。
  • exit:元素从「开始离开视口」到「完全离开视口」。
  • cover:元素和视口从有交集到完全没交集的整个过程。

entry 0% entry 100% 的意思就是:动画在「刚冒头」到「完全进来」之间跑完。如果要让动画更从容,可以把终点往后推到 cover 30% 之类的位置,卡片会在视口里走得更深一点才缓缓归位。

关于 both 这个填充值,有个小地方容易被忽略。卡片此时还在视口下方很远的地方,进度是 0%,如果不写 backwards 或者 both,浏览器会直接渲染元素的默认样式——也就是完全可见。那卡片在进入视口之前就已经是「已揭示」状态,动画自然就看不到了。而 to 那端其实不需要填充,因为动画结束后元素状态和默认状态本来就一致。所以严格来说这里写 backwards 就够,写 both 只是习惯,不算错。

四、第三个例子:hero 图视差

视差在以前是个 JS 性能测试题,现在几行 CSS:

.hero-frame {
  overflow: hidden;
  block-size: 24rem;
}

.hero-frame img {
  inline-size: 100%;
  block-size: 130%;
  object-fit: cover;
  animation: hero-drift linear both;
  animation-timeline: view();
  animation-range: cover;
}

@keyframes hero-drift {
  from { translate: 0 -10%; }
  to   { translate: 0 10%; }
}

图片高度做到容器的 130%,多出来的部分正好留给位移,外面的 overflow: hidden 负责裁掉超出的区域。animation-range: cover 是整个覆盖区间,图片从能看到到看不见,位移匀速走完,视差感就出来了。

这里只动 translate,属于合成阶段就能处理的属性,不触发布局也不触发重绘,这是它能扛住连续滚动的原因。

五、第四个例子:把时间轴借给别的元素

这是这套特性里我目前觉得最容易被低估的部分。假设页面是两栏:左边是一个可以独立滚动、内容很长的文章列表,右边侧边栏里有一个小标记,希望它随着左边列表的滚动而移动,跟页面本身的滚动没关系。

难点在于,侧边栏的标记和滚动容器是兄弟关系,时间轴默认的作用域管不到它。这时候用 timeline-scope 把时间轴名字提到共同祖先上:

.layout {
  /* 把 --list-scroll 这个名字提升到这一层,兄弟也能引用 */
  timeline-scope: --list-scroll;
}

.article-list {
  overflow-y: auto;
  block-size: 100%;
  scroll-timeline: --list-scroll block;
}

.side-marker {
  animation: marker-slide linear both;
  animation-timeline: --list-scroll;
}

@keyframes marker-slide {
  from { translate: 0 0; }
  to   { translate: 0 8rem; }
}

默认情况下,scroll-timeline 声明的名字只在声明它的那个元素以及它的后代里可见。侧边栏是它的兄弟,看不到这个名字,所以需要 timeline-scope 在更高一层把名字「借出来」。这个作用域的规则和 CSS 变量的作用域思路是一致的,理解了变量再理解这个会快很多。

六、实际项目里一定会踩的坑

1. 千万别把 animation-timeline 塞进 animation 简写。第一次写的时候我图省事,写成 animation: reveal linear view(),结果浏览器把 view() 当成了动画名,页面一动不动,排查了大半天。简写里没有时间轴的位置,必须单独一行声明。

2. animation-duration 会被忽略。不要指望写个 0.3s 让动画快点结束。控制快慢的手段只有两个:调整 animation-range 的区间,或者重新分配关键帧的百分比。

3. 滚动源选错是无声的失败。很多项目会在 body 上设 overflow: hidden,然后让一个内部 div 承担全部滚动。这种情况下 scroll(root) 会一直停在 0,进度条纹丝不动,控制台也不报错。正确做法是在那个真正滚动的容器上声明具名时间轴,再用 timeline-scope 透出来,就像上面第五节的写法。

4. 元素高度会改变 view() 的节奏。cover 区间的长度等于视口高度加上元素高度,元素一变高,整个动画的分布就变了。这和 JS 里按滚动距离算进度完全是两码事。如果卡片高度不固定,最好在 animation-range 里用百分比把它约束在一个稳定位置,而不是依赖绝对像素。

5. 读不到进度值。滚动驱动动画把进度计算交给了 CSS,也就意味着你没法像 IntersectionObserver 那样在回调里拿到一个 0~1 的数字。真需要读,得回到 JS:element.getAnimations()[0].currentTime。所以判断标准很清楚——纯表现层的东西用它,一旦需要读进度做逻辑分支,还是老实写观察器。

6. 减少动效偏好要处理。视差、位移、缩放这类会引起不适的动画,在 prefers-reduced-motion: reduce 下应该关掉。进度条这类纯信息指示可以保留,也可以一并简化,看产品取舍。

七、渐进增强的写法,别让旧浏览器开天窗

目前 Chrome、Edge 支持得很完整,Safari 的新版本也已经跟上,Firefox 仍在推进中。真实项目里,用 @supports 把动画和初始状态一起保护起来:

/* 基样式里千万不要写 opacity: 0 */
.card {
  opacity: 1;
}

@supports (animation-timeline: view()) {
  .card {
    opacity: 0;
    animation: card-reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .card {
    opacity: 1;
    animation: none;
  }
}

这中间有个真实的生产事故模式:把 opacity: 0 写在不受 @supports 保护的基样式里,然后指望动画把它变回来。在不支持滚动驱动动画的浏览器上,动画根本不会跑,卡片就永久隐身了。所以初始隐藏状态必须和动画写在同一个特性查询块里,同进同退。

八、调试怎么弄

写这类动画不建议靠反复滚动页面看效果,一是慢,二是很难停在某个精确位置观察。开发者工具的 Animations 面板现在会把这些滚动驱动动画列出来,并且提供一个可以直接拖动的进度控件,比手工滚动高效得多。定位问题时,先确认动画有没有出现在列表里——如果压根没出现,那大概率是 animation-timeline 的写法被解析错了,先回去检查简写那个坑。

写在最后

滚动驱动动画真正改变的是一种分工:过去我们把「滚动」当成输入事件,一切都要经过 JS 中转;现在滚动本身就是一条时间轴,CSS 可以直接挂上去。代价是你放弃了对进度的读取权,换来的是主线程的清净。

判断该用哪种方案,我自己的标准很朴素:只影响表现、不需要中途做判断的,交给 CSS;需要根据位置切换内容、加载数据、改变结构的,继续用 IntersectionObserver。两套东西不冲突,混着用才是常态。

CSS滚动驱动动画实战:用animation-timeline做阅读进度条与卡片揭示
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 css CSS滚动驱动动画实战:用animation-timeline做阅读进度条与卡片揭示 https://www.taomawang.com/web/css/2804.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务