你有没有写过这样的代码:一个列表要做交错入场动画,你写了一长串
:nth-child(1)到:nth-child(20),每项延迟不同;或者用JavaScript遍历所有子元素,手动给每个元素打上style="--i: 3",还得在列表增删时重新算一遍。如果列表长度不确定,你甚至得写一个MutationObserver来监听DOM变化,动态更新那个--i。2026年8月18日,Firefox 154正式发布。这一天,CSS的
sibling-index()和sibling-count()成为了Baseline Newly Available。从Chrome 138、Edge 138,到Safari 26.2,再到Firefox 154,主流浏览器全部支持了这两个函数。CSS终于学会了"自己数数"。 那些为了"知道自己是第几个"而写的JavaScript,可以从你的项目里删掉了。
一、先说清楚:这两个函数到底返回什么
sibling-index() 返回当前元素在同级兄弟元素中的位置 ,从1开始,就像 :nth-child() 一样。sibling-count() 返回父元素的子元素总数,计数时包含当前元素自身。两者都不接受参数,只统计元素节点,中间的文本节点和注释不影响结果。
它们和 counter() 最本质的区别是:返回的是整数,可以直接参与 calc() 数学运算 。counter() 给你的是一个只能通过 content 打印的字符串,而 sibling-index() 和 sibling-count() 给你的是能做加减乘除的数字。
一个返回"你在哪里",一个返回"一共有多少"。 就这两个数字,足以替代过去大量需要JavaScript介入的场景。
二、第一个被"干掉"的东西::nth-child() 阶梯
交错入场动画是最经典的例子。过去你可能会这样写:
css
li:nth-child(1) { animation-delay: 0ms; }
li:nth-child(2) { animation-delay: 60ms; }
li:nth-child(3) { animation-delay: 120ms; }
/* ......写到手酸 */
或者用JavaScript遍历:
javascript
list.querySelectorAll('li').forEach((el, i) => {
el.style.setProperty('--i', i);
});
现在,两行CSS就够了:
css
li {
animation: fade-in 0.4s backwards;
animation-delay: calc(sibling-index() * 60ms);
}
@keyframes fade-in {
from { opacity: 0; translate: 0 8px; }
}
没有逐项规则,没有脚本,没有 --i。 如果列表中间插入一个新元素,它后面的所有项会自动重新计算索引和延迟。你不需要手动更新任何东西。
Thoughtbot的工程博客记录了他们用这两个函数"修剪"CSS的实践:把那些按位置写死的 nth-child 声明和手动维护的 --item-position 变量全部删掉,代码量明显下降,而且列表增删再也不用担心样式错位。
三、真正的杀手级场景:数学布局
交错动画只是"省事",但 sibling-index() 和 sibling-count() 真正让人眼前一亮的地方,是用纯CSS做数学布局。
圆形菜单:每个元素知道自己的角度
css
li {
--a: calc(360deg / sibling-count() * (sibling-index() - 1));
transform: rotate(var(--a)) translateY(-128px) rotate(calc(-1 * var(--a)));
}
三行CSS,就得到了一个完美的环形布局。rotate(var(--a)) 把元素转向它的"槽位",translateY(-128px) 把它推到半径上,第二个 rotate() 把标签转正,保持可读。删除任何一个元素,剩下的元素会自动获得新的索引和新的角度,配合 transition 平滑移动到新位置。
扇形和向日葵:同样的原语,不同的数学
扇形布局的核心是让索引围绕零居中:
css
--offset: calc(sibling-index() - (sibling-count() + 1) / 2);
transform: rotate(calc(var(--offset) * 10deg)) translateY(-200px);
向日葵布局则用黄金角(137.5°)和平方根距离:
css
--angle: calc(137.5deg * sibling-index());
--radius: calc(sqrt(sibling-index() / sibling-count()) * 200px);
Smashing Magazine的一篇深度文章用这两个函数构建了从简单交错到复杂树形数学布局的完整案例集,结论是:同样的原语,配合 calc() 和CSS数学函数,可以覆盖"5个元素到5000个元素"的规模。
这些布局过去需要JavaScript计算坐标、写入 transform,现在全部可以在样式表里完成。
四、供应链看板:动态统计卡片"零JS"生成
华为云社区的一篇实战文章展示了一个更"企业级"的场景:供应链数据看板上的动态统计卡片。
传统实现中,卡片数量、每个卡片的位置和动画都需要JavaScript介入。数据更新时,JS要重新计算、重新渲染、重新绑定动画。用 sibling-index() 和 sibling-count(),卡片布局和动画完全由CSS驱动:卡片的排列、间距、入场延迟都基于它"是第几张"和"一共有几张"自动计算。数据层只需要增删DOM节点,样式层自动适配。
这个案例的意义不在于"供应链看板"本身,而在于它展示了一个模式:当"位置信息"成为CSS的一等公民时,视图层可以从大量"计算位置"的逻辑中解放出来。 数据驱动DOM,CSS驱动表现,JavaScript退回它该待的地方------处理业务逻辑和数据流。
五、一个你必须知道的坑:@property 和自定义属性
这两个函数很直观,但有一个陷阱,几乎每个第一次用的人都会踩。
你可能会想:把索引存到自定义属性里,方便子元素复用。
css
li {
--i: sibling-index();
}
li > button::before {
content: counter(idx); /* 想用 --i 来做计数 */
}
这不会按你预期工作。 普通自定义属性存储的不是"数字",而是 sibling-index() 这个标记(token) 。当子元素读取 var(--i) 时,sibling-index() 会在子元素自己的上下文 里重新求值------它拿到的是子元素在同级中的位置,不是父元素 li 的位置。
正确的做法是用 @property 把自定义属性注册为整数:
css
@property --i {
syntax: "<integer>";
inherits: true;
initial-value: 1;
}
li {
--i: sibling-index();
}
注册之后,--i 会在声明它的元素(li)上完成解析,子元素继承的是已经算好的整数值,而不是原始标记。
如果你打算把索引或总数传递到子元素,@property 不是可选项,是必选项。
六、其他边界:Shadow DOM、插槽和回退
Shadow DOM是隔离的。 如果一条规则试图从Shadow Tree外部通过 ::part() 去计数Shadow Tree内部的元素,函数会返回 0。这是刻意设计,防止Shadow DOM结构泄露到外部。
插槽内容按Light DOM计数。 和 :nth-child() 一样,这两个函数工作在DOM树上,不是扁平化后的渲染树。<slot> 分发的内容按它在Light DOM中的位置计数,而不是它在Shadow DOM中实际渲染的位置。
回退仍然有意义。 Baseline Newly Available意味着当前版本的主流浏览器都支持,但老版本用户还需要回退方案。一个特性检测就够了:
javascript
if (!CSS.supports('width', 'calc(1px * sibling-index())')) {
const items = list.children;
for (let k = 0; k < items.length; k++) {
items[k].style.setProperty('--i', k + 1);
items[k].style.setProperty('--n', items.length);
}
}
因为样式表只读取 var(--i) 和 var(--n),JavaScript回退只需要把这两个变量填上,其余CSS逻辑完全不用改。内联样式优先级高于样式表,所以JS的值会覆盖 sibling-index() 的声明。
七、这对前端开发者意味着什么?
1. "位置"从JavaScript的私有领域变成了CSS的公共能力
过去,"知道自己是第几个"是JavaScript的活。你遍历、你计数、你写内联样式、你监听DOM变化。CSS只能被动地接收你算好的结果。
sibling-index() 和 sibling-count() 把这件事翻转了。位置信息成为样式系统的一等公民 ,CSS可以自己读、自己算、自己在 calc() 里用。JavaScript不再需要充当"计数中间人"。
2. 设计系统的 :nth-child 阶梯可以清理了
几乎每个设计系统里都有一堆 :nth-child(1) 到 :nth-child(n) 的规则,用来处理"第几个元素的特殊样式"。这些规则是"位置耦合"的------列表长度一变,规则就得跟着变。
用这两个函数,规则可以写成位置无关 的形式:样式表达的是"基于你在哪里、一共有多少"的函数关系,而不是"如果你是第3个,就长这样"。设计系统从"枚举位置"走向"描述关系"。
3. 性能上的账:CSS比JS循环更"便宜"
当DOM发生增删或重排时,浏览器会在级联阶段 为受影响的兄弟元素重新计算样式。这个过程发生在渲染管线内部,比JavaScript循环遍历、逐元素写内联样式、再触发一次样式重算要更早、更连贯。成本仍然随树的大小增长,但省掉了JS执行和样式写入之间的往返。
不是说CSS计算"免费",而是它把工作放到了更合适的地方。
4. AI生成前端代码时,少了一类"必须靠猜"的事情
当AI Agent生成一个交错动画或圆形布局时,过去它需要决定"用 nth-child 阶梯还是用JS循环"。这两种选择都有各自的不确定性:阶梯的长度、JS的边界条件。
现在,一个标准的 calc(sibling-index() * 60ms) 就是正确答案。CSS原语越强,AI生成的代码越可预测。
写在最后
2026年8月18日,sibling-index() 和 sibling-count() 成为Baseline Newly Available。
它们不做惊天动地的事。它们只是让CSS知道了"我是第几个"和"一共有几个"。但正是这两个数字,让 :nth-child 阶梯可以退役,让JavaScript计数逻辑可以删除,让交错动画、圆形菜单、扇形布局、向日葵排列这些"数学布局"从脚本搬回了样式表。
CSS不再只是"描述长什么样"。它开始"理解结构,并基于结构做计算"。
参考链接: