配了八年列表页,每次加个入场动画都要重算 delay——今天 CSS 自己会认路了

写过列表页的工程师,大概都踩过这个坑——给五个卡片做入场动画,需要算出每个卡片的 animation-delay:第一个 0ms,第二个 100ms,第三个 200ms……

然后设计师说:”能不能改成六个?”然后你改完,产品说:”还是去掉第二个吧。”然后你就崩溃了——每个卡片的 delay 都要手动改一遍。

这个场景,CSS 终于有原生解了。

浏览器现在自己知道你在第几个

sibling-index() 是 CSS Values and Units Level 5 里的新函数,Chrome 126+ 和 Safari 18+ 已经稳定支持,2026 年 8 月正式纳入 Baseline 2026——意味着主流浏览器全面覆盖,你可以直接在生产环境用。

它的用法特别简单:

li {
  animation-delay: calc(sibling-index() * 100ms);
}

一个列表里无论有多少项,每一项的 delay 会自动根据自己在兄弟节点中的位置计算。第一项 100ms,第二项 200ms,第三项 300ms——不用传 index prop,不用写 nth-child 规则,不用写一行 JS。

sibling-count() 是它的兄弟函数,返回同级兄弟节点的总数。它可以把一个容器里的项目数变成 CSS 变量用:

.tab {
  width: calc(100% / sibling-count());
}

三个 tab 各占 33.33%,加到五个就自动变成 20%,删到两个就变成 50%——整个计算过程在 CSS 层面完成。

这两个函数解决了什么问题

过去做”交错动画”(staggered animation)有三个办法,每个都有代价:

办法一:内联 style 属性

{items.map((item, index) => (
  <li style={{ animationDelay: `${index * 100}ms` }}>{item}</li>
))}

问题:DOM 里塞了样式逻辑,改设计要动 JS 代码。

办法二:CSS 预处理器循环

用 Sass 生成 20 条 nth-child 规则覆盖 1-20 项的 delay。

问题:超过 20 项就破,还要在构建时生成大量 CSS。

办法三:ResizeObserver + JS

动态读取列表长度,计算每项宽度。

问题:运行时开销,DOM 变化要重新计算。

sibling-index() 把这个问题的本质暴露出来了:浏览器在构建 DOM 树的时候,本来就知道每个元素是第几个——它只是没把这个信息暴露给 CSS。现在它暴露了。

三个真实可落地的场景

场景一:列表入场动画

这是最直接的使用场景。配合 @starting-style(CSS 入场动画专用伪元素),可以实现列表项依次淡入,零 JS:

.card {
  opacity: 0;
  animation: fade-in 0.4s ease both;
  animation-delay: calc(sibling-index() * 80ms);
}

@starting-style .card {
  opacity: 0;
}

@keyframes fade-in {
  to { opacity: 1; }
}

5 个卡片从第一张开始依次淡入,最后一张比第一张晚 320ms。用户看到的是一组”依次出现”的效果,而不是一起蹦出来。

如果想反过来——让最后一项先出现,先消失——算个简单减法:

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

最后一项 delay = (N – N) × 80ms = 0ms,第一项 delay = (N – 1) × 80ms,视觉上就是从尾到头倒放。

场景二:动态等宽 Tab

过去写 Tab 组件,等宽要用 flex: 1 或者 JS 动态计算。用 sibling-count() 一行搞定:

.tab-item {
  width: calc(100% / sibling-count());
}

容器里有几个 tab,每个 tab 就自动占几分之一。设计师临时加一个进来,每个 tab 宽度自动重新计算。

这个模式还有个进阶用法——色轮分布。把色相平均分配给同级所有项:

.swatch {
  background-color: hsl(
    calc((360deg / sibling-count()) * sibling-index())
    70% 50%
  );
}

三个项各占 120° 间隔,十二个项各占 30°,颜色分布自动均匀。

场景三:CSS 圆形排列

以前要把一组元素围成一个圆,要用 JS 算每个元素的 x/y 坐标:

items.forEach((item, i) => {
  const angle = (360 / items.length) * i;
  item.style.left = `calc(50% + ${radius * Math.cos(angle)}%)`;
  item.style.top = `calc(50% + ${radius * Math.sin(angle)}%)`;
});

现在纯 CSS:

.radial-item {
  --angle: calc((360deg / sibling-count()) * sibling-index());
  --radius: 120px;
  position: absolute;
  left: calc(50% + var(--radius) * cos(var(--angle)));
  top: calc(50% + var(--radius) * sin(var(--angle)));
}

CSS 本身就带了 sin() 和 cos(),配合 sibling-index(),整个圆形排列在 CSS 层完成。六个项就是六边形,八个项就是八边形,动态加减项自动重排。

一个重要限制

sibling-count() 目前还没进 Baseline——Firefox 还没支持,Safari 也没跟。所以生产环境里如果你只用 sibling-index(),完全没问题;如果需要 sibling-count() 做动态宽度,可以用 @supports 做渐进增强:

.tab-item {
  width: 20%; /* fallback */
}
@supports (width: sibling-count(0)) {
  .tab-item {
    width: calc(100% / sibling-count());
  }
}

sibling-index() 本身就是 Baseline 2026 了,可以直接用,不需要 @supports 包裹。

另外,sibling-index()sibling-count() 有几个细节值得注意:

  • 它们只数元素节点,文本节点和注释不计入
  • ::before::after 伪元素不算兄弟节点
  • Shadow DOM 里的元素只能看到 Shadow Tree 内部的结构,light DOM 的内容对它不可见——这是安全边界,不是 bug

怎么判断要不要迁移

如果你现在用 JS 在做这三件事,可以评估一下要不要换成 sibling-index():

第一,列表入场动画用了 index prop 或者内联 style;第二,Tab 或者栅格布局用了 JS 计算等宽;第三,任何根据同级项目数量做动态布局的场景。

sibling-index() 是 2026 年 CSS 清单里最接近”删除 JavaScript”这个目标的功能之一。它不依赖任何 polyfill,不破坏任何旧浏览器,渐进增强有 @supports 保底。

下一次设计师说”给这个列表加个入场动画”,你可以直接回:”CSS 自己会算。”

评论区

0 条评论

登录后可评论。

阿柯·前端架构 16 阅读