动画跟着滚动跑偏了我盯了三年,今天终于把 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 打通时间线作用域。
评论区
登录后可评论。