这两个CSS函数其实已经喊了好几年,就像外卖点了“即将送达”却一直看不到骑手影子。简单讲,sibling-index()能返回一个元素在同级中的第几个位置(从1开始数),sibling-count()则返回父元素下总共有多少个同级元素。有了这俩,做交错动画、根据元素数量改背景色之类的事情就不用再跟JS死磕。虽然规范里已经躺着了,浏览器也表了态要搞,但目前还是没法直接上手用。别急,下面几套野路子照样能模拟出一样的效果。
核心概念
索引值(sibling-index)好比排队时拿到的号码牌,第一个是1,第二个是2,以此类推。计数值(sibling-count)则像问售票员“这一排总共多少人”,得到的就是总数量。当前CSS里nth-child()能选中某个位置的元素,:has()能判断是否有多少个孩子,但就是没法把这两个数字存下来当变量用。而这两个函数恰好返回的是整数,可以直接塞进calc()做数学运算。比如想让第三个之后的元素背景变红,或者让列表项按序号递增延迟动画,有了它们就是分分钟的事。
硬编码大法
最笨但最稳的法子,就是一条一条把索引写死。拿<li>列表举例,HTML长这样:
<ol>
<li>苹果</li>
<li>香蕉</li>
<li>橘子</li>
</ol>CSS里挨个点名:
li:nth-child(1) {
--sibling-index: 1;
}
li:nth-child(2) {
--sibling-index: 2;
}
li:nth-child(3) {
--sibling-index: 3;
}计数值稍微麻烦点,得借助:has()做数量查询。比如检测只有1个元素时:
ol:has(> :last-child:nth-child(1)) {
--sibling-count: 1;
}
ol:has(> :last-child:nth-child(2)) {
--sibling-count: 2;
}
ol:has(> :last-child:nth-child(3)) {
--sibling-count: 3;
}要是列表有12项,光索引和计数就得写24条规则,手都能敲断。但好处是没有任何黑魔法,浏览器兼容性杠杠的。写完这些自定义属性后,就能直接在样式里用var(--sibling-index)和var(--sibling-count)了。
嵌套写法
硬编码时可以把计数规则塞进索引规则里,省点行数。还是以第2项为例:
li:nth-child(2) {
--sibling-index: 2;
ol:has(> &:last-child) {
--sibling-count: 2;
}
}注意这个写法看着像把爹塞进了儿子里面,但CSS完全合法。:has(> &:last-child)的意思是:如果当前li是父元素ol的最后一个孩子,那就给ol设置--sibling-count。每个索引规则里都这样嵌套一遍,虽然规则总数没变,但维护时不用在两个地方来回跳了。
对数跳转法
这招来自Roman Komarov的思路,用两条自定义属性拼出索引,规则数量从线性增长变成对数增长。比如设定一个“因子”为3,那么只用4条规则就能覆盖最多8个元素。先看基础设置:
li {
--si1: 0;
--si2: 0;
--sibling-index: calc(3 * var(--si2) + var(--si1));
}然后写两组选择器。第一组处理--si1,按照因子倍数偏移:
li:nth-child(3n + 1) { --si1: 1; }
li:nth-child(3n + 2) { --si1: 2; }第二组处理--si2,用区间选择器批量赋值:
li:nth-child(n + 3):nth-child(-n + 5) { --si2: 1; }
li:nth-child(n + 6):nth-child(-n + 8) { --si2: 2; }这么一套组合拳下来,第一个元素:3*0+1=1,第二个:3*0+2=2,第三个:3*1+0=3,第四个:3*1+1=4……一直到第八个:3*2+2=8。计数值同理,在容器上设置--sc1和--sc2,再配合:has()塞进上面的每条规则里。比如:
li:nth-child(3n + 1) {
--si1: 1;
ol:has(> &:last-child) { --sc1: 1; }
}因子越大能覆盖的元素越多。选因子10,只需要写10条左右的规则就能覆盖99个元素。这方法就像用打火石生火,数学上有点绕,但懂了之后贼好用。
JS观察者方案
要是觉得上面那些CSS魔法太烧脑,直接用MutationObserver监听DOM变化,几行JS搞定无限数量。先拿到容器元素:
const container = document.querySelector("ol");写一个更新函数,遍历所有子元素设置--sibling-index,同时把子元素总数设给容器的--sibling-count:
const updateProps = () => {
let idx = 1;
for (let child of container.children) {
child.style.setProperty("--sibling-index", idx);
idx++;
}
container.style.setProperty("--sibling-count", container.children.length);
};初始化调用一次:
updateProps();最后挂上观察器,只监听子节点增删:
const observer = new MutationObserver(updateProps);
observer.observe(container, { childList: true });这样不管往列表里加多少个<li>,或者删掉几个,自定义属性都会实时刷新。这个方法像火焰喷射器,简单粗暴但效率极高。性能方面不用担心,现代浏览器处理几百个元素轻轻松松。
| 方案 | 规则数量 | 最大支持数 | 上手难度 |
|---|---|---|---|
| 硬编码 | 2N条 | 无上限 | 低 |
| 对数跳转 | 约2√N条 | 因子平方减1 | 中 |
| JS观察者 | 1个函数 | 无上限 | 低 |
等到官方函数正式落地那天,直接全局搜一把替换就行:
ol {
--sibling-count: sibling-count();
}
li {
--sibling-index: sibling-index();
}