文章索引

滚动驱动动画:让 scroll 监听器退休

· 2 分钟

滚动驱动动画:让 scroll 监听器退休

很长一段时间里,「滚动到哪,动画走到哪」意味着 JS 监听 scroll 事件、算百分比、改样式。这条路径每一步都在主线程上,稍有不慎就是掉帧。CSS 滚动驱动动画把这件事整个搬进了浏览器内部。

核心概念只有一个

animation-timeline 允许把动画的时间轴从「时钟」换成「滚动位置」。动画本身还是普通的 @keyframes,变的只是驱动它的东西。

例子一:阅读进度条

本站文章页顶部的进度条没有一行 JS:

css
.reading-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--accent);
  transform-origin: 0 50%;
  animation: grow linear both;
  animation-timeline: scroll(root);
}

@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

scroll(root) 表示时间轴来自根滚动容器。页面滚到底,动画正好播完。

例子二:元素入场

view() 时间轴更进一步:动画进度由「元素在视口中的位置」决定,相当于内置了 IntersectionObserver 加补间:

css
.card {
  animation: rise linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 80%;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

animation-range 限定动画只在元素进入视口的前 80% 行程内播放,之后保持最终状态。

渐进增强与可访问性

截至写作时,Chromium 系浏览器与 Safari 都已支持,Firefox 仍需手动开启旗标。所以要包一层 @supports,让不支持的浏览器直接看到最终状态:

css
@supports not (animation-timeline: scroll()) {
  .reading-progress { display: none; }
}

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

进度条属于状态指示,隐藏即可;入场动效属于装饰,在用户声明减少动态时应当彻底关闭。

什么时候还需要 JS

需要根据滚动做逻辑判断(比如目录高亮当前章节)时,IntersectionObserver 依然是正确工具。原则很简单:表现层交给 CSS 时间轴,语义层交给观察者。