标题听起来有点奇怪,对吧?为什么要重新实现一个现有的功能,这完全没用!我有两个理由。第一个原因是能够动态调整 :nth-child() 的参数。

假设你有以下内容:

div:nth-child(5n + 8) {
  background: red;
}

然后你决定更新 5 或 8。唯一的方法是手动重写选择器,这并不理想。我们无法像下面这样,让你随意更改变量:

.container {
  --A: 5;
  --B: 8;
}

.container > div:nth-child(var(--A)*n + var(--B)) {
  background: red;
}

使用我的实现,你就可以做到这一点!

第二个原因是为了好玩并尝试一些新功能。你可能永远不需要依赖动态的 :nth-child() 选择器,但我确信你会在这个过程中学到一两个有用的技巧。

我们将依赖相同的代码结构在所有示例中:一个包含一堆 <div> 元素的容器。

<div class="container">
  <div></div>
  <div></div>
  <div></div>
  <!-- ... -->
</div>

而容器控制 :nth-child() 的变量:

.container {
  --A: ;
  --B: ;
}

.container > div {
  /* 根据 --A 和 --B 的值更改背景值 */
  background: ??;
}

我并不是说这种结构是实施所必需的。我只是考虑了一个简单的结构以保持易于理解。

:nth-child(B)

让我们从最简单的情况开始,一个只选择一个元素的选择器:

div:nth-child(5) { }

我想我不需要解释这个选择器的作用。5 只是所选元素的索引。所以,如果 B 等于元素的索引,该元素就会被选中。

我们可以将其转换为以下 CSS 代码:

.container {
  --B: 5;
}

.container div {
  background: if(style(sibling-index() = --B): red); 
}

这是有效的 CSS 代码吗?!

是的,它是有效的,并且可以工作!

我正在使用新的 if() 语法,它允许我在 style() 内定义条件。如果该条件为真,则背景色设置为红色。否则,什么都不发生。

我们也可以有 if/else:

.container {
  --B: 5;
}

.container div {
  background: if(style(sibling-index() = --B): red; else: lightblue); 
}

if() 语法并不复杂。在我们的情况下,我们只有一个条件,所以你可以这样看:

.container div {
  background: 
    if(style( condition ): <color_if_condition_true>; 
    else: <color_if_condition_false>); 
}

棘手的部分是如何在 style() 内定义条件。我写了一篇小文章解释这部分,所以在继续之前,请给自己一个好处去阅读它:“Style Queries 和 if() 的隐藏技巧”。这是一篇快速阅读,包含关键细节,有助于你更好地理解 if() 的工作原理。从这里,你会发现我们的条件可以有几种写法,都是有效的:

.container div {
  background: if(style(sibling-index() = --B): red);
  background: if(style(sibling-index() = var(--B)): red);
  background: if(style(var(--B) = sibling-index()): red); 
  background: if(style(--B = sibling-index()): red); 
}

我们有了第一个实现!你也可以轻松地将它应用于任何属性:

.container {
  --B: 5;
}

.container div {
  background: if(style(sibling-index() = --B): red; else: lightblue);
  color: if(style(sibling-index() = --B): white);
}

代码看起来有点冗余,这可能是该方法的一个缺点,但实现选择器并能够动态调整 --B 的值仍然很酷。不要忘记我们的目标是学习新技巧,你已经掌握了两个(sibling-index() 和 if())。

这里有一个交互式演示,你可以调整 --B 的值并查看变化:

:nth-child(n + B)

让我们进入下一个选择器。它选择多个元素。

div:nth-child(n + 5) { }

想一想如何实施这样的选择器。它与前一个几乎相同。

你想到了吗?我希望如此!

.container {
  --B: 5;
}

.container div {
  background: if(style(sibling-index() >= --B): red; else: lightblue);
}

如果元素的索引大于或等于 --B 的值,则该元素被选中。就这么简单!

:nth-child(-n + B)

那这个呢?很简单,对吧?我们只需将 >= 替换为 <=!

.container {
  --B: 5;
}

.container div {
  background: if(style(sibling-index() <= --B): red; else: lightblue);
}

我们还可以轻松实现范围选择,即这两个选择器的组合:

/* 选择 [X Y] 范围内的任何元素 */
div:nth-child(n + X):nth-child(-n + Y) {
  background: red;
}

它转换为:

.container {
  --X: ;
  --Y: ;
}

.container div {
  background: if(style(--X <= sibling-index() <= --Y): red);
}

代码应该是自解释的。它甚至比 :nth-child() 符号更直观,如果你不习惯的话,后者需要几分钟来破译。

:nth-child(An)

现在让我们处理 A 参数。例如,以下选择器:

div:nth-child(3n) { }

…选择第 3、6、9 等元素。换个角度看,元素的索引必须是 3 的倍数。要进行此类测试,我们可以使用 mod() 函数。如果 mod(index, 3) 返回 0,这意味着 index 除以 3 的余数等于 0,因此它是 3 的倍数。

.container {
  --A: 3;
}

.container div {
  background: if(style(mod(sibling-index(), var(--A)) = 0): red);
}

它有效!另一个具有简单实现的 selector。

我们可以识别两种特殊情况。当 --A 等于 1 时,我们有 :nth-child(n),它选择所有元素;当它等于 2 时,我们有 :nth-child(2n),这也是 :nth-child(even)。

虽然这工作得很好,但有一个小陷阱。当 A 是负值时,选择器应该不选择任何元素,这在逻辑上是正确的,但如果你尝试在我的实现中使用负值,一些元素仍然会被选中,这是错误的。

mod() 函数可以与负数一起工作,在这种情况下仍可能返回 0,因此为了更准确,我们应该测试 A 的符号。

.container {
  --A: 3;
}

.container div {
  background: 
    if(style( (mod(sibling-index(), var(--A)) = 0) and (var(--A) > 0): red);
}

看到了吗?我只是在 style() 内使用 and 关键字添加了另一个条件。if() 的语法乍一看可能令人困惑,但最终它与其他任何语言一样:在 style() 内定义的一堆条件。你可以拥有任意数量的条件,并用 and 和 or 组合。

现在,我们的选择更精确了。

:nth-child(An + B)

现在让我们结合到目前为止所学的所有技巧来实现 :nth-child() 的通用形式。在这种情况下,索引等于 An + B,我们可以这样表达 n:

n = (index - B)/A

n 应该是一个正整数(包括零)。对于正数部分,我们可以使用:

(index - B)/A >= 0

对于整数部分,我们需要确保 index - B 是 A 的倍数,为此我们使用 mod():

mod(index - B, A) = 0

我们将此转换为 CSS 以获得以下内容:

.container {
  --A: 3;
  --B: 5;
}

.container div {
  background: 
    if(style( (mod(sibling-index() - var(--B), var(--A)) = 0) 
      and (calc((sibling-index() - var(--B))/var(--A)) >= 0) 
    ): red);
}

我们几乎完成了,除了当 --A 等于 0 的情况。在这种情况下,选择是完全错误的。mod() 的结果为 NaN,因为其第二个参数等于 0,这将破坏条件。实际上,该条件本就不适用于 --A 等于 0 的情况。

我们要么认为此实现仅对不等于 0 的 --A 值有效(这可能正是我们想要的),要么我们尝试找到一种解决方案来包含这种情况。

我们是来学习和发现新事物的,所以让我们尝试别的方法。可能有多种方法来修复 0 的情况,但这是我的想法:

.container {
  --A: 3;
  --B: 5;
}

.container div {
  --n: calc((sibling-index() - var(--B))/var(--A));
  --g: if(style(--A = 0):
         calc(sign(sibling-index() - var(--B)) + 1);
       else:
         calc(sign(var(--n) + 1) + mod(var(--n),1))
       );
  background: if(style(--g = 1): red);
}

它是如何工作的?好吧,这将是你的作业!

如您所见,我引入了一个新变量,当元素被 :nth-child(An + B) 选中时,该变量应等于 1。请尝试拆解代码以理解其工作原理。如果您仍感到困难,请查看以下文章:“使用 sibling-index() 和if() 实现动态 nth-child()。”

以下是最终交互示例,其中完整实现了 :nth-child(An + B):

我们只需调整变量即可轻松获取所有之前的选择器:

一系列项目中的第七个子项被高亮显示为红色。
:nth-child(7)
31 个元素系列中的第 10 至第 31 个元素被高亮显示为红色。
:nth-child(n + 10)
31 个项目系列中的前十个被高亮显示为红色。
:nth-child(-n + 10)
31 个项目系列中每第三个元素被高亮显示为红色。
:nth-child(3n)

结论

希望您喜欢这个小练习。这是一个探索新 if() 功能的好机会,而 :nth-child() 选择器正是为此量身打造的。毕竟,选择器不过是一组用于过滤元素并选中所需元素的条件集合。

那 :nth-last-child() 呢?如果您愿意接受另一个挑战,请尝试看看如何更新之前的实现,使其能与 :nth-last-child() 配合工作。(提示:您可能需要依赖 sibling-count()。)