12k
All articles

无需 nth-child 计算的交错动画

用sibling-index()替代nth-child规则和JavaScript循环,实现CSS错峰动画。了解反向与居中效果、布局用法、浏览器支持及兼容方案。

OpenReplay Team
OpenReplay Team
无需 nth-child 计算的交错动画

只需一条规则 animation-delay: calc((sibling-index() - 1) * 80ms),就能为任意长度的列表实现交错动画。你不必再为每一项单独写 :nth-child() 规则,也不必再用 JavaScript 循环去设置 --i。

大多数交错动画代码都是按五个元素写的。之后有人加了第六张卡片,结果它和第一张卡片同时淡入。

本文要做的就是删掉这类代码。全文围绕同一个示例展开:一组卡片依次淡入,每张都比前一张稍晚出现。你将看到旧的实现方式、替代方案、反向与从中心向外扩散的变体、sibling-count() 在布局中的用法,以及一个让代码可以放心上线的防护措施。本文只讨论 CSS。如需 React 方案,请参阅使用 Framer Motion 实现交错文字动画。

核心要点

  • sibling-index() 返回元素在其兄弟元素中的位置,从 1 开始计数。sibling-count() 返回父元素的子元素总数,计数时包含该元素自身。
  • 两个函数都返回纯数字。只有在 calc() 中乘以某个单位后,它们才会变成时间值或长度值。
  • 无论列表有多长,animation-delay: calc((sibling-index() - 1) * 80ms) 都能取代为每张卡片单独编写的 :nth-child() 规则。
  • 这两个函数先后在 Chrome 和 Edge 138(2025 年 6 月)、Safari 26.2(2025 年 12 月)和 Firefox 154(2026 年 8 月)中发布,因此已达到 Baseline “新近可用”(newly available)状态。
  • 将延迟声明放进 @supports (top: calc(1px * sibling-index())) 中。这样,不支持这两个函数的浏览器会让所有卡片同时淡入,页面不会出错。

旧方案:nth-child 阶梯与内联自定义属性

在 sibling-index() 出现之前,用 CSS 实现交错动画意味着要为每个位置硬编码一个延迟值。常见做法有两种:一是 :nth-child() 阶梯,二是在标记中写入索引自定义属性。阶梯写法使用 :nth-child()。它和大多数开发者熟悉的 CSS 兄弟选择器一样,根据元素在兄弟元素中的位置来选中元素:

.card { animation: fade-in 400ms ease both; }
.card:nth-child(2) { animation-delay: 80ms; }
.card:nth-child(3) { animation-delay: 160ms; }
.card:nth-child(4) { animation-delay: 240ms; }
.card:nth-child(5) { animation-delay: 320ms; }

这种阶梯写到哪一项,就只能覆盖到哪一项。自定义属性方案则把计数工作转移到了 HTML 中:

<li class="card" style="--i: 0">…</li>
<li class="card" style="--i: 1">…</li>
<li class="card" style="--i: 2">…</li>
<li class="card" style="--i: 3">…</li>
.card { animation-delay: calc(var(--i) * 80ms); }

对于动态列表,最后通常还得用脚本来设置索引:

document.querySelectorAll('.card').forEach((el, i) => el.style.setProperty('--i', i));

sibling-index() 如何取代 nth-child 阶梯?

sibling-index() 只需一条声明,就能取代整个 :nth-child() 阶梯:animation-delay: calc((sibling-index() - 1) * 80ms)。该函数定义于 CSS Values and Units Level 5 的树计数函数(tree-counting functions)中。sibling-index() 与 :nth-child() 一样,将第一个兄弟元素编号为 1,并且在计数时会跳过文本节点和注释节点。

@keyframes fade-in {
  from { opacity: 0; translate: 0 8px; }
}

.card {
  animation: fade-in 400ms ease both;
  animation-delay: calc((sibling-index() - 1) * 80ms);
}

该函数返回的是一个不带单位的整数,正是 calc() 中的乘法运算把它转换成了时间值。

减去 1 是为了让第一张卡片没有延迟。以五张卡片为例,索引 1 到 5 分别对应 0、80、160、240 和 320ms。如果不减 1,每张卡片都会多等一个步长,整个列表会先静止 80ms 才开始动。

简写中的 both 关键字同样重要。当 animation-fill-mode 设为 both 或 backwards 时,第一个关键帧会在延迟期间生效,因此等待中的卡片会保持不可见。如果省略它,后面的卡片会先以完全不透明的状态显示,等到各自的动画开始时再突然跳到 0。

对于长列表,可以给延迟设置上限,以免第 40 张卡片要等待三秒多:

.card { animation-delay: calc(min(sibling-index() - 1, 10) * 80ms); }

如何实现反向或从中心扩散的交错效果?

要反转 sibling-index() 交错动画,只需用 sibling-count() 减去索引。calc((sibling-count() - sibling-index()) * 80ms) 会让最后一张卡片的延迟为 0ms,于是最后一张卡片最先播放动画,第一张卡片最后播放。若要实现从中心向外扩散的效果,则计算每张卡片与中间位置的距离:

.card { --centre: calc((sibling-count() + 1) / 2); }

.list--reverse .card {
  animation-delay: calc((sibling-count() - sibling-index()) * 80ms);
}

.list--centre .card {
  animation-delay: calc(
    max(sibling-index() - var(--centre), var(--centre) - sibling-index()) * 80ms
  );
}

有五张卡片时,中心值为 3,延迟依次为 160、80、0、80 和 160ms。当卡片数量为偶数时,中心落在两张卡片之间。例如四张卡片的延迟依次为 120、40、40 和 120ms,中间两张卡片会同时开始。

在布局中使用 sibling-count()

sibling-count() 也可以用作布局值。根据 MDN 参考文档的说明,它统计父元素的所有子元素,包括该元素自身。用它做除数即可实现等分:width: calc(100% / sibling-count())。下面将它应用到同一个卡片列表上,并把间距也计算在内:

.list { display: flex; --gap: 1rem; gap: var(--gap); }

.card {
  width: calc((100% - (sibling-count() - 1) * var(--gap)) / sibling-count());
  --progress: calc(sibling-index() / sibling-count() * 100%);
}

.card::after { content: ""; display: block; height: 3px; width: var(--progress); }

在普通的 flex 行布局中,flex: 1 本身就能实现等宽。这个公式适用于 flex 无能为力的场景,例如绝对定位或相互重叠的卡片,或者宽度需要参与其他计算的情况。--progress 为每张卡片计算其在总数中的占比,因此 5 张卡片中的第 3 张会绘制一条 60% 宽的进度条。

sibling-count() 和 sibling-index() 统计的是元素自身的兄弟元素,而不是它的子元素。如果在 <ul> 上使用 sibling-count(),得到的是该列表父元素的子元素数量(包括列表本身),而不是列表中 <li> 的数量。与子元素数量相关的计算,应当写在子元素上。

上线部署:浏览器支持与 @supports 防护

sibling-index() 和 sibling-count() 已在三大浏览器引擎中全部落地,因此属于 Baseline 新近可用特性。

浏览器引擎版本发布时间
Chrome / Edge1382025 年 6 月
Safari(macOS、iOS)26.22025 年 12 月
Firefox1542026 年 8 月

仍有用户在使用旧版本浏览器,因此应将淡入动画放在防护块之外,只把交错延迟放在防护块之内:

.card { animation: fade-in 400ms ease both; }

@supports (top: calc(1px * sibling-index())) {
  .card { animation-delay: calc((sibling-index() - 1) * 80ms); }
}

这个检测条件把函数包裹在一个计算结果为长度的 calc() 中,并将其应用于一个接受长度值的属性。无法解析该声明的浏览器会将条件判定为 false,并跳过整个代码块。所有卡片依然会淡入,只是同时出现而已。

尊重 prefers-reduced-motion 设置

在 prefers-reduced-motion: reduce 条件下,为卡片设置 animation: none,即可让所有卡片直接以最终状态显示:

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

总结

现在,交错效果只需一条带防护的声明,适用于任意数量的卡片。删除 :nth-child() 阶梯和写入 --i 的脚本,加上 @supports 代码块和减弱动效规则,你的标记中就不再需要索引属性了。如果你还有更多动画仍依赖 JavaScript,请参阅用原生 Web API 替代动画库。

常见问题

sibling-index() 与 CSS 的 counter() 函数有什么区别?

counter() 生成的是文本,因此只能在 content 属性中使用。sibling-index() 生成的是可以参与数学运算的数字,例如 calc((sibling-index() - 1) * 80ms)。计数器的值无法驱动 animation-delay、宽度或角度,而 sibling-index() 可以,而且无需编写 counter-reset 或 counter-increment 规则。

sibling-index() 能否像 :nth-child(An+B of S) 那样跳过某些元素?

不能。sibling-index() 和 sibling-count() 不接受任何参数,因此无法按选择器进行过滤。它们会统计同一父元素下的所有兄弟元素,无论其 class 是什么。列表中如果夹杂标题、分隔线或包裹元素,其后每张卡片的索引都会偏移,总数也会增加。请确保动画元素是其容器中唯一的子元素。

sibling-index() 除了驱动延迟,还能驱动颜色和旋转吗?

可以。这个整数可以用在任何 calc() 表达式中,乘以某个单位即可得到角度或长度,例如 rotate: calc(sibling-index() * 15deg)。除以 sibling-count() 则可以将某个值均匀分配到整个列表中。例如,calc(sibling-index() / sibling-count() * 360deg) 可以为每一项分配各自的色相,或各自在圆周上的位置。

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.