配了八年列表页,每次加个入场动画都要重算 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 自己会算。”
评论区
登录后可评论。