Scrubbable Stagger:单个进度值驱动错开动画
传统的错开动画 (staggered animation) 靠给每个元素叠不同的 animation-delay——但那是时间概念,只能正向播放,没法被一个外部进度「拖着走」。本页复刻
Frontend Masters 的一篇文章的思路:把「延迟」从时间维度改写成进度维度的偏移——整组动画只由一个标量 --m (0→1) 驱动,第 i 个元素在总进度里减去
i/k 的偏移就「起步更晚」。于是这组错开动画可被任意 scrub:接到滑块、接到滚动进度都行,正放、倒放、停在任意中间帧。封装手段是 CSS 的 @function at-rule,序号靠 sibling-index() 自动取得。
1 · 拖进度条:一个 --m 驱动整排级联
下面一排条的高度都由同一个 --stagger(var(--m), var(--k)) 算出。拖进度滑块改 --m,看级联前沿如何从左扫到右;拖密度 --k 看相邻条的重叠变多还是变少(k
越大、同时在动的条越多、过渡越平滑);勾上缓动则把每根的线性进度再过一道 --smoothstep()。
每根条只调一次函数、--i 默认取 sibling-index(),写法与位置无关:
.bar {
--p: --stagger(var(--m), var(--k));
height: calc(var(--p) * 100%);
}
2 · 公式拆解
--stagger 内部是一段
:先用 --lerp 把整体进度 --m 映射成一条扫过所有元素的「前沿」位置,再减去本元素的偏移 i/k,夹回 [0, 1] 就是这一根自己的进度。起点锚 (--u, --x) 与终点锚 (--v, --y) 决定前沿在 m=0 /
m=1 两端落在哪——取默认值时,起始帧第一根刚要动、结束帧最后一根恰好满。
@function --lerp(--a, --b, --x) {
result: calc(var(--a) + (var(--b) - var(--a)) * var(--x));
}
@function --stagger(
--m, --k: 5, --i: sibling-index(),
--u: 1, --x: 0, --v: sibling-count(), --y: 1
) {
--q1: calc(var(--x) + var(--u) / var(--k));
--q2: calc(var(--y) + var(--v) / var(--k));
result: clamp(0, --lerp(var(--q1), var(--q2), var(--m)) - var(--i) / var(--k), 1);
}
各参数的含义:
| 参数 | 含义 |
|---|---|
--m |
整体进度 [0, 1],唯一的驱动量;接滑块、接滚动、接 @keyframes 都行。 |
--k |
错开密度——大致是「同时在动的元素数」。k 越大,相邻元素重叠越多、整体越平滑;k 越小越像逐个点亮。 |
--i |
本元素的序号,默认 sibling-index() (从 1 起,自动按它在兄弟里的位置取得,无需手写)。 |
--u / --x |
起点锚:在 --m = 0 时,第 --u 个元素恰好处于进度 --x。默认 1 / 0。 |
--v / --y |
终点锚:在 --m = 1 时,第 --v 个元素恰好处于进度 --y。默认 sibling-count() / 1。 |
3 · 把 --m 接到滚动:同一个函数,换个驱动源
「可 scrub」的意义在这里显出来:上面用滑块喂 --m,这里把驱动源换成滚动进度——给 --m 写一段 @keyframes stagger-frames { to { --m: 1 } },再把它的时间线设成
scroll()。函数、HTML、每根条的写法一个字都不用改,级联就跟着滚动条走(往回滚即倒放)。底层机制见本系列 Scroll Timeline。
@property --m { syntax: "<number>"; inherits: true; initial-value: 0; }
@keyframes stagger-frames { to { --m: 1; } }
.row {
animation: stagger-frames linear both;
animation-timeline: scroll(nearest);
}
现状提醒: 本页用到的两组特性进展并不同步。@function 出自 CSS Functions and Mixins 模块,仍属实验特性——仅 Chromium 139+(2025-08)落地,Firefox 与 Safari 均未实现。sibling-index() / sibling-count() 出自 CSS Values 5,已走得更远:Chromium
138+(2025-06)与 Safari 26.2+(2025-12)均已落地,Firefox 在 preview 阶段,规范上也不再标记为实验特性。因此真正的短板是 @function。本页在支持的浏览器跑真 @function + sibling-index() + animation-timeline: scroll();否则退回 JS
按同一公式计算每根条的高度(并在读数里标注)。探测用运行期探针(临时插一条 @function 看 computed 值),而非 @supports。
4 · 相关链接
- Scrubbable Staggered Animation with CSS @function · frontendmasters.com——本页出处:把 stagger 改写成单一进度偏移、用
@function --stagger封装、再接到 scroll 的完整推导。 - @function · developer.mozilla.org——at-rule 语法:参数类型、默认值、
result描述符与局部 custom property。另见本仓 css-functions · @function。 - sibling-index() / sibling-count() · developer.mozilla.org——让元素在样式层自取「我是第几个 / 共几个」,本页的逐元素偏移就靠它。
- CSS Functions and Mixins Module Level 1 · drafts.csswg.org——
@function的规范本体。