等CSS新特性等到花都谢了,sibling-index()和sibling-count()到底咋整?

2,544字
11–16 分钟
in

这两个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();
}