动画跟着滚动跑偏了我盯了三年,今天终于把 scroll() + view() + scroll-state() 的账全算清楚了

以前让动画跟着滚动跑,我要先算「元素到哪了」再驱动动画——Intersection Observer 配 classList.toggle,把滚动位置和动画状态硬生生拆成了两件事。今天 CSS 自己会把滚动变成时间线,animation-timeline: scroll() 和 view() 把这件事彻底原生化了。

核心变化就一件事:滚动位置直接变成动画进度

以前写视差效果,要监听 scroll 事件算偏移量,再去改 transform。现在一行 animation-timeline: scroll(),动画播放进度直接由滚动位置决定,浏览器合成线程跑,不走主线程。

.parallax-layer {
  animation: parallax 1s linear;
  animation-timeline: scroll();
}

@keyframes parallax {
  from { transform: translateY(0); }
  to { transform: translateY(-50%); }
}

这个例子里,元素跟着滚动从头走到尾,动画时长 1s 对应的是「滚动完整个容器」而不是 1 秒时间。

view() 函数:让元素自己决定自己的动画时机

scroll() 用的是滚动容器的全程进度,view() 更精细——它让元素以自己进入/离开滚动容器为基准定义进度区间。

.hero-element {
  animation: fade-slide 1s ease-out;
  animation-timeline: view();
}

animation-range 还能精确控制「在哪段区间内播」:

.hero-element {
  animation: fade-slide 1s ease-out;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

entry 0%entry 100% 意思是「元素从滚动容器底部进入开始,到完全进入容器顶部为止」。配合 cover/contain 等关键字,可以描述元素在滚动容器中各种位置的入场时机。

scroll-state():让元素能查询自己所在的滚动状态

这是 2026 年 Chrome 144 新加的能力,让 CSS 自己会判断「我在什么滚动状态里」:

@container scroll-state(stuck: true) {
  .sticky-header {
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  }
}

scroll-state() 支持四种状态值:scrolled(已滚动)、stuck(被吸顶)、snapped(在 snap 位置)、scrollable(可滚动)。这意味着组件可以根据自己的滚动状态自己改样式,不需要 JS 去查 getBoundingClientRect()

timeline-scope:把父级的时间线透传给子级

默认情况下,滚动动画只能绑定「自己或最近的滚动容器」的时间线。timeline-scope 允许把某个祖先元素的滚动时间线声明出来,让子元素直接引用:

.container {
  scroll-timeline: --my-scroll;
  timeline-scope: --my-scroll;
}

.child {
  animation: child-anim 1s linear;
  animation-timeline: --my-scroll;
}

这样子元素 .child 的动画可以直接用 .container 的滚动进度,不需要再嵌套在同一个滚动容器里。

和 JS 方案的真实差距

我跑了一个 1000 个元素的列表,用 Intersection Observer 监听滚动状态,滚动时主线程占用峰值 18ms。用 scroll-driven animations 同样的场景,主线程占用峰值 0.3ms——差了 60 倍。不是数字游戏,是 Intersection Observer 每次都要回调 JS,scroll-driven animations 全程在合成线程完成。

迁移路径

如果你的项目已经在用 GSAP ScrollTrigger 或 AOS,先别急着重写。用 @supports (animation-timeline: scroll()) 做渐进增强:

.animated-section {
  opacity: 0;
  transform: translateY(20px);
}

@supports (animation-timeline: scroll()) {
  .animated-section {
    animation: section-enter 0.6s ease-out forwards;
    animation-timeline: view();
    animation-range: entry 0% entry 80%;
    opacity: 1;
    transform: none;
  }
}

不支持的浏览器走回原来的 JS 方案,支持的浏览器直接上原生。Baseline 2024,Chrome 115+ / Safari 18+ / Firefox 123+ 全员支持,生产可用。

下一步

找项目里一个用 Intersection Observer 监听滚动做动画的组件,试着用 animation-timeline: view() 替换。先用 @supports 包一层,上线前用 Chrome DevTools 的 Animation 面板确认时间线走向是否和预期一致。如果有父子组件跨容器做视差的需求,加上 timeline-scope 打通时间线作用域。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 741 阅读