令人惊讶的 CSS 选择器


最近,我在学习 CSS 嵌套的过程中深入研究了新的 & 选择器,特别是与 :is() 选择器的关系。我听说它的行为类似于 :is() 选择器,在研究的过程中,我学到了很多关于这些新选择器如何工作的知识。

首先要知道的是,:is() 及其兄弟 :not():has() 和 :where() 是一种新的选择器类型,称为函数伪类。我们长时间以来一直有简单的伪类,比如 :hover,但是将选择器列表作为参数传递的能力是将这四个伪类区分为一个新类别的关键点。

让我们先简要回顾一下今天将要讨论的新选择器,然后我们将深入探讨我学到的一些令人惊讶的事情:

  • 任意匹配伪类 :is() 接受一个以逗号分隔的选择器列表,并匹配列表中任何一个选择器可以选择的元素。例如,:is(article, section, aside) h1 将匹配包含在 article、section 或 aside 元素中的任何 h1 元素。
  • 否定(或不匹配任何)伪类 :not() 表示不匹配选择器列表的元素。例如,li:not(:last-child) 将选择列表中不是最后一个项目的任何列表项。
  • 层级关系伪类 :has() 是一种根据其子元素或兄弟元素选择父元素的方式。例如,h1:has(+ p) 只会应用于紧随其后有 p 元素的 h1 元素。
  • 调整特异性的伪类 :where() 的行为类似于 :is() 选择器,但其特异性始终为零。
  • 嵌套选择器 & 表示 CSS 嵌套中父规则匹配的元素,在幕后,它被视为一个 :is() 选择器。

    选择优先级

    这些新选择器最有趣的之一是它们与选择优先级的关系。 ID 的优先级高于类,而类的优先级高于元素选择器。
    1
    2
    3
    #unique { color: red; }
    .intro { color: orange }
    p { color: green; }
    1
    <p class="intro" id="unique">This will be red</p>

这个段落将是红色的,因为 ID 选择器是最高优先级的。

那么对于接受逗号分隔的选择器列表的伪类,比如 :is():not() :has() 会发生什么呢?伪类的优先级由列表中最具体的选择器确定。

1
2
:is(#unique, p) { color: red; }
.intro { color: green; }
1
<p class="intro">This will also be red</p>

你可能期望段落是绿色的,因为它没有 #unique ID。但在这种情况下,列表中最具体的对象是 ID,因此它将成为 :is() 选择器的权重。因此,即使在这种情况下 ID 不适用,它仍会影响特异性。

这种行为的一个有趣变化是 ·:where() ·选择器,它的行为与 :is() 选择器完全相同,唯一的区别是它始终具有零权重。

1
2
:where(#unique, .intro) { color: red; }
p { color: green; }
1
<p class="intro" id="unique">This will be green.</p>

这在编写希望易于覆盖的 CSS 时特别有用,例如 WordPress 主题或第三方库中的默认样式。

选择器列表

在标准 CSS 中构建选择器列表时,如果列表中的任何选择器是无效的,比如 .valid-class :invalid-pseudo-class,整个样式块都将被忽略。

现在,你可能会想“谁会在他们的 CSS 中添加无效的选择器呢?”你可能需要这样做的一个例子是浏览器前缀的选择器。比如,如果你想使用 :fullscreen 伪类,但需要支持一些只理解 webkit 前缀版本的旧浏览器,你需要编写如下代码:

1
2
:-webkit-full-screen { border-color: green; }
:fullscreen { border-color: green; }

如果尝试使用逗号分隔的列表,那么不理解 :fullscreen 的浏览器会失败。因此,你必须为每个选择器重复样式块。

值得庆幸的是,大多数新的伪类,比如:is():where(),都接受规范称之为宽容的选择器列表,这意味着列表中的任何无效选择器都将被忽略,但仍将使用有效的选择器。

1
:is(.valid-class, :invalid-pseudo-class) { ... }

浏览器会忽略 :invalid-pseudo-class,但仍然应用规则到 .valid-class。

不幸的是,:not() 伪类使用的是不宽容的选择器列表,因此将任何无效选择器添加到列表中将导致整个样式块被忽略。然而,有一个相当愚蠢的解决方案:只需将选择器列表包装在:is() 伪类中:

1
:not(:is(.valid-class, :invalid-pseudo-class)) { ... }

这将使 :is() 从列表中剥离任何无效的选择器,并将剩余的选择器传递给 :not()

直到最近,:has()伪类也使用了宽容的选择器列表,但由于与 jQuery 存在冲突,他们不得不在 2022 年底撤销这一更改。现在,如果可能有一个无效的选择器传递给 :has(),你可以使用相同的 :has(:is()) 技巧来防止它破坏。

需要注意的事项

关于这些新选择器,如果不小心的话可能会让你感到惊讶。以下是一些我注意到的事项:

  • 不能使用新选择器选择伪元素。写 a:is(::before, ::after) 是无效的。这样做的原因比较复杂。
  • 要小心!.a .b .c 不同于.a :is(.b .c)。第一个匹配任何 .c,它是 .b 的子元素,而 .b 是 .a 的子元素。第二个匹配任何 .c,它是 .a 和 .b 的子元素,而不考虑顺序!在这篇由 Bramus Van Damme 撰写的优秀文章中学到更多。
  • :not() 选择器将匹配“非 X”的所有内容。例如,body :not(table) a 仍将应用于 内的链接,因为 、、
    等都可以匹配选择器的 :not(table) 部分。
  • 你可能会遇到一些较旧的文章,其中提到 :not() 只能接受简单的选择器,如果需要定位多个项目,你需要像 :not(.foo):not(.bar) 这样链式调用它们。这是过时的。选择器规范的第四版已更新语法,现在可以接受选择器列表,因此现在你可以这样写::not(.foo, .bar)。
  • 你可以通过在 @supports 规则中使用 selector() 函数来测试对新选择器的支持,如下所示:@supports selector(:is(*))。虽然 selector() 函数本身相对较新,但它得到了很好的支持。

    嵌套选择器

    好的,让我们谈谈我为什么要深入研究这个话题的原因——& 嵌套选择器!这个新选择器是为了 CSS 嵌套而添加的(尽管它也可以用于其他上下文,比如 CSS 作用域)。在 CSS 嵌套中使用时,& 将被替换为父样式规则的选择器,其选择优先级与是否使用 :is() 相同
    1
    2
    3
    4
    5
    6
    a {
    &:hover { color: rebeccapurple; }
    }

    /* will be treated like */
    :is(a):hover { color: rebeccapurple; }
  • 在大多数情况下,这不会有任何影响,只是值得注意,但可能会产生一些的副作用。

    1
    2
    3
    4
    5
    6
    #card, .card {
    .featured & { color: cornflowerblue; }
    }

    /* will be treated like */
    .featured :is(#card, .card) { color: cornflowerblue; }

    由于 & 选择器包含一个 ID,生成的规则将使用该 ID 的优先级。因此,在使用 & 时,要注意潜在的意外优先级冲突。

    值得注意的是,由于嵌套选择器的行为类似于 :is() 选择器,它也不能表示伪元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    a, a::before, a::after {
    color: red;

    &:hover { color: blue; }
    }

    /* will be treated like */
    a, a::before, a::after {
    color: red;
    }
    a:hover {
    color: blue;
    }

    正如你所看到的,伪元素在最终规则的悬停状态中被忽略了。

    结论

    我开始学习一下 CSS 嵌套的工作原理,特别是新的 & 选择器,结果却深入研究了:is()选择器及其衍生物。希望这对你也有用。