CSS 滚动驱动动画实战:用 scroll() 和 view() 干掉 scroll 监听

2026-10-30 0 516

项目里有个阅读型页面,顶部要一条随滚动增长的进度条,正文里的卡片要边滚边淡入。按老办法,一个 scroll 监听加 requestAnimationFrame 节流,再挂几个 IntersectionObserver,能跑,但每次滚动都在主线程上算样式,低端机上肉眼可见地掉帧。

去年底 Chrome 把滚动驱动动画(Scroll-driven Animations)的 scroll() 和 view() 正式发版之后,这块东西基本可以从 JS 里整个删掉。我把那个页面重写了一遍,JS 从六十多行降到零。下面是从查文档到跑通的全过程,包括几个卡了我一阵子的坑。

先说清楚它到底在干什么

普通 CSS 动画的时间轴是文档时间:从 animation 开始计时,按秒往前走。滚动驱动动画做的事,是把时间轴换成了别的东西——

scroll():把某个滚动容器的滚动进度当作时间轴。滚到顶是 0%,滚到底 100%。

view():把元素自身在视口里露出的程度当作时间轴。元素刚碰到视口下边缘是 0%,完全穿过视口是 100%。

关键在于,这个进度是浏览器在合成线程上算的,不经过主线程。滚动的时候主线程就算被一堆 JS 堵着,动画照样丝滑。这是它和 scroll 事件 + JS 改样式最本质的区别——不是写法更短,而是执行路径根本不同。

案例一:顶部阅读进度条

整个滚动容器是页面本身,所以时间轴绑在根滚动容器上。

<div class="progress"></div>

.progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, #f97316, #ec4899);
  transform-origin: 0 50%;
  transform: scaleX(0);
  animation: fill linear;
  animation-timeline: scroll(root block);
}

@keyframes fill {
  to { transform: scaleX(1); }
}

几个细节说明一下。

scroll(root block) 里的 root 指的是根滚动容器,block 是块轴方向——竖排文档就是竖直方向,哪天要做横向翻页的进度条就换成 inline。

用 scaleX 而不是改 width。改宽度会触发重排,虽然这里是合成层上的动画也能跑,但宽度的变化会连带动布局,没必要。配合 transform-origin: 0 50%,缩放从左边长出去,效果和宽度增长一模一样。

animation 简写里没写时长。滚动驱动动画不需要 animation-duration,写了也会被忽略。这里 linear 是必须的,因为默认的 ease 会让进度条在开头和结尾变速,看起来像卡了一下。

还有 transform: scaleX(0) 这行初值。如果页面加载时已经处于滚动位置(比如刷新后浏览器恢复滚动位置),动画会立刻按下当前进度渲染,不会出现先看到一条满格再缩回去的闪烁。

案例二:卡片滚入淡出

正文里的卡片,要求是“进入视口 60% 时刚好完全显示,完全离开视口时再淡出”。这次用 view()。

.card {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 20% exit 40%;
}

@keyframes reveal {
  from { opacity: 0; transform: translateY(28px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}

animation-range 是这套东西里最需要花点时间理解的部分。它规定了动画从时间轴的哪个位置开始、到哪个位置结束。entry、exit、cover、contain 这几个名字分别对应元素穿过视口的不同阶段:

cover:从元素开始碰到视口,到完全离开视口,整个生命周期。

entry:从元素开始进入视口,到完全进入视口。

exit:从元素开始离开视口,到完全离开视口。

所以 entry 20% exit 40% 的意思是:进入视口 20% 的位置开始播,离开视口 40% 的位置播完。中间的 contain 阶段动画停在最后一帧。

这里带了个 both。因为卡片默认应该在视口下方时是隐藏的,而动画的 from 关键帧里才是隐藏状态,所以必须让它在 animation-range 开始之前就保持在 from。不加 both,卡片在你滚到它之前会以正常状态显示出来,然后滚到触发点突然闪一下消失再淡入——这个现象我第一版就中招了。

五个坑,按遇到的先后顺序排

坑一:animation-timeline 写在 animation 前面就没了

这条最阴。

/* 错误写法 */
.card {
  animation-timeline: view();
  animation: reveal linear both;
}

animation 是简写属性,它会把自己包含的所有子属性重置成初始值,而 animation-timeline 就在这个重置范围里。浏览器按书写顺序解析,先设置的时间轴立刻被后面的简写干掉,动画于是回退到文档时间轴,元素瞬间播完。表现出来就是“什么都没发生”。

规矩很简单:animation-timeline 永远写在 animation 之后。或者干脆全程用长写属性,别碰简写。

坑二:scroll() 找不到滚动容器就是死

我给一个内部滚动区域做进度条,直接把 scroll() 挂上去了,结果一动不动。原因是 scroll() 不带参数时默认绑定最近的滚动祖先,而那个区域外层只有一个 overflow: auto 忘了设高度、实际上没滚起来的 div。没有滚动条就没有进度,动画从头到尾停在 0%。

这种场景用命名时间轴更稳,把名字定义在滚动容器上,动画绑名字,不用靠“最近祖先”去猜:

.panel {
  overflow-y: auto;
  height: 420px;
  scroll-timeline: --panel block;
}

.panel-progress {
  animation: fill linear;
  animation-timeline: --panel;
}

坑三:降级不写,旧版 Safari 上整页元素不显示

这是最要命的一个。移动端还有不少设备跑不认 animation-timeline 的浏览器。这些浏览器读到 animation: reveal linear both 里的 both,会把 from 关键帧的状态直接套上去,而且永远不会往前走——结果就是所有卡片永久停留在 opacity: 0。页面看起来像坏了一样,白花花一片。

解决办法是把整套动画包进特性查询,不支持的浏览器压根看不到这些规则:

@supports (animation-timeline: view()) {
  .card {
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry 20% exit 40%;
  }

  @keyframes reveal {
    from { opacity: 0; transform: translateY(28px) scale(0.97); }
    to   { opacity: 1; transform: none; }
  }
}

注意 @keyframes 我也一起放进去了。虽然单独放外面通常也能跑,但放在里面语义更清楚:不支持时间轴的环境下,这些关键帧本来就用不上。

坑四:prefers-reduced-motion 之后元素又集体消失

无障碍这块要处理,但处理方式不能只是把动画关掉。前面说了,from 是隐藏状态,你把动画整个去掉,元素倒是显示了;可如果你只是把 animation 的时长改成 0s 或者置成 none 而保留了 both,元素就会卡在 from 上。

正确的做法是把动画整体撤掉,让元素回到默认样式:

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

坑五:animation-play-state: paused 不是用来做延迟的

我想让卡片“稍微晚一点”开始淡入,下意识写了 animation-play-state: paused 想去卡住它。没用。滚动驱动动画的进度完全由滚动位置决定,play-state 只能整体冻结,不会让它“晚点开始”。延迟是靠 animation-range 的起始百分比调的,比如改成 entry 30%,就自然晚了。

完整可跑的页面

把上面几段合起来,一个文件就能验证效果。滚动条拖到底,进度条满格,卡片依次淡入淡出,全程没有一行 JS。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
  body {
    margin: 0;
    font-family: system-ui, sans-serif;
    line-height: 1.8;
    padding: 0 20px;
    background: #0f1115;
    color: #d8dee9;
  }
  article { max-width: 640px; margin: 0 auto; padding: 40px 0 80vh; }

  .progress {
    position: fixed; inset: 0 0 auto 0; height: 3px;
    background: linear-gradient(90deg, #f97316, #ec4899);
    transform: scaleX(0);
    transform-origin: 0 50%;
  }

  .card {
    background: #1a1e26;
    border-radius: 12px;
    padding: 24px;
    margin: 32px 0;
    border: 1px solid #262b36;
  }

  @keyframes fill { to { transform: scaleX(1); } }

  @keyframes reveal {
    from { opacity: 0; transform: translateY(28px) scale(0.97); }
    to   { opacity: 1; transform: none; }
  }

  @supports (animation-timeline: scroll()) {
    .progress {
      animation: fill linear;
      animation-timeline: scroll(root block);
    }
  }

  @supports (animation-timeline: view()) {
    .card {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry 20% exit 40%;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .progress, .card { animation: none; transform: none; opacity: 1; }
  }
</style>
</head>
<body>
<div class="progress"></div>
<article>
  <h1>滚动试试</h1>
  <div class="card">第一张卡片</div>
  <div class="card">第二张卡片</div>
  <div class="card">第三张卡片</div>
</article>
</body>
</html>

要不要现在就用

纯展示类的滚动效果——进度条、视差分层、元素入场、章节标题吸顶——基本可以放心上了,配合 @supports 降级之后,老浏览器只是少了动效,内容一点不受影响,这个代价完全可以接受。

但如果动画里要出现计算结果、要根据业务数据动态变形,或者需要跟用户交互(比如拖拽、点击)产生联动,那还是老老实实用 JS。animation-timeline 只是把“时间从哪来”这件事交给浏览器,它并不打算取代业务逻辑。

另外还有两个方向值得留意:一个是 scroll-state() 查询,能让样式直接响应“是否吸顶”“是否滚到边界”这类状态;另一个是给动画显式指定命名时间轴之后的链式组合。等这两个在主流浏览器上稳下来,滚动相关的 CSS 大概还能再省掉一批 JS。

CSS 滚动驱动动画实战:用 scroll() 和 view() 干掉 scroll 监听
收藏 (0) 打赏

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

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

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

淘吗网 css CSS 滚动驱动动画实战:用 scroll() 和 view() 干掉 scroll 监听 https://www.taomawang.com/web/css/2984.html

常见问题

相关文章

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

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