:has()

Baseline 2023
新推出

自 ⁨2023 年 12 月⁩起,此功能可在最新的设备和浏览器版本上使用。此功能可能无法在较旧的设备或浏览器上使用。

功能性 :has() CSS 伪类表示一个元素,其条件是:作为参数传递的相对选择器在与该元素关联时,至少匹配一个元素。这个伪类通过接受一个相对选择器列表作为参数,提供了一种相对于参考元素来选择父元素或前一个兄弟元素的方法。

css
/* Selects an h1 heading with a
paragraph element that immediately follows
the h1 and applies the style to h1 */
h1:has(+ p) {
  margin-bottom: 0;
}

:has() 伪类的优先级由其参数中优先级最高的选择器决定,与 :is():not() 的方式相同。

语法

css
:has(<relative-selector-list>) {
  /* ... */
}

如果浏览器不支持 :has() 伪类本身,那么整个选择器块都将失效,除非 :has() 位于一个宽容选择器列表(forgiving selector list)中,例如 :is():where()

:has() 伪类不能嵌套在另一个 :has() 中。

伪元素在 :has() 中也不是有效的选择器,同时伪元素也不是 :has() 的有效锚点。这是因为许多伪元素的存在是基于其祖先元素的样式来决定的,允许 :has() 查询它们可能会引入循环查询。

示例

选择父元素

你可能正在寻找一种“父组合器”,它允许你沿着 DOM 树向上查找并选择特定元素的父元素。:has() 伪类通过使用 parent:has(child)(对于任何父元素)或 parent:has(> child)(对于直接父元素)来实现这一点。这个例子展示了如何为一个包含具有 featured 类的子元素的 <section> 元素设置样式。

html
<section>
  <article class="featured">Featured content</article>
  <article>Regular content</article>
</section>
<section>
  <article>Regular content</article>
</section>
css
section:has(.featured) {
  border: 2px solid blue;
}

结果

与兄弟组合器一起使用

下面例子中的 :has() 样式声明调整了 <h1> 标题后的间距,条件是它后面紧跟着一个 <h2> 标题。

HTML

html
<section>
  <article>
    <h1>Morning Times</h1>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
  <article>
    <h1>Morning Times</h1>
    <h2>Delivering you news every morning</h2>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
</section>

CSS

css
h1,
h2 {
  margin: 0 0 1rem 0;
}

h1:has(+ h2) {
  margin: 0 0 0.25rem 0;
}

结果

这个例子并排展示了两个相似的文本进行比较——左边是一个 H1 标题后跟着一个段落,右边是一个 H1 标题后跟着一个 H2 标题,然后再跟着一个段落。在右边的例子中,:has() 帮助选择了紧跟一个 H2 元素(由相邻兄弟组合器 + 指示)的 H1 元素,CSS 规则减小了这样一个 H1 元素后面的间距。如果没有 :has() 伪类,你无法使用 CSS 选择器来选择一个不同类型的前置兄弟元素或父元素。

与 :is() 伪类一起使用

这个例子在之前例子的基础上,展示了如何使用 :has() 选择多个元素。

HTML

html
<section>
  <article>
    <h1>Morning Times</h1>
    <h2>Delivering you news every morning</h2>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
  <article>
    <h1>Morning Times</h1>
    <h2>Delivering you news every morning</h2>
    <h3>8:00 am</h3>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
</section>

CSS

css
h1,
h2,
h3 {
  margin: 0 0 1rem 0;
}

:is(h1, h2, h3):has(+ :is(h2, h3, h4)) {
  margin: 0 0 0.25rem 0;
}

结果

这里,第一个 :is() 伪类用于选择列表中的任何标题元素。第二个 :is() 伪类用于将一个相邻兄弟选择器列表作为参数传递给 :has():has() 伪类帮助选择了任何后面紧跟着(由 + 指示)一个 H2H3H4 元素的 H1H2H3 元素,并且 CSS 规则减小了这些 H1H2H3 元素后面的间距。

这个选择器也可以写成

css
:is(h1, h2, h3):has(+ h2, + h3, + h4) {
  margin: 0 0 0.25rem 0;
}

逻辑运算

:has() 关系选择器可以用来检查多个特性中是否有一个为真,或者是否所有特性都为真。

通过在 :has() 关系选择器中使用逗号分隔的值,你正在检查是否存在任何一个参数。x:has(a, b) 会在后代 ab 存在时为 x 设置样式。

通过将多个 :has() 关系选择器链接在一起,你正在检查是否所有参数都存在。x:has(a):has(b) 会在后代 ab 都存在时为 x 设置样式。

css
body:has(video, audio) {
  /* styles to apply if the content contains audio OR video */
}
body:has(video):has(audio) {
  /* styles to apply if the content contains both audio AND video */
}

:has() 与正则表达式的类比

有趣的是,我们可以将一些 CSS :has() 结构与正则表达式中的先行断言(lookahead assertion)联系起来,因为它们都允许你根据一个条件来选择元素(或正则表达式中的字符串),而实际上并不选择匹配该条件的元素(或字符串)本身。

正向先行断言 (?=pattern)

在正则表达式 abc(?=xyz) 中,只有当字符串 abc 后面紧跟着字符串 xyz 时,abc 才会被匹配。由于这是一个先行断言操作,xyz 不会包含在匹配结果中。

在 CSS 中类似的结构是 .abc:has(+ .xyz):它仅在存在一个相邻兄弟元素 .xyz 时才选择元素 .abc:has(+ .xyz) 部分起到了先行断言的作用,因为它选择的是 .abc 元素,而不是 .xyz 元素。

负向先行断言 (?!pattern)

同样地,对于负向先行断言的情况,在正则表达式 abc(?!xyz) 中,只有当字符串 abc 后面xyz 时,abc 才会被匹配。类似的 CSS 结构 .abc:has(+ :not(.xyz)) 在下一个元素是 .xyz 时,不会选择 .abc 元素。

性能注意事项

:has() 伪类的某些用法会显著影响页面性能,尤其是在动态更新(DOM 变更)期间。当 DOM 发生变化时,浏览器引擎必须重新评估 :has() 选择器,而复杂或约束不佳的选择器可能导致昂贵的计算。

避免宽泛的锚点

锚点选择器(A:has(B) 中的 A)不应该是拥有过多子元素的元素,例如 body:root*。将 :has() 锚定到非常通用的选择器会降低性能,因为在广泛选择的元素的整个子树中,任何 DOM 变化都需要浏览器重新检查 :has() 条件。

css
/* Avoid anchoring :has() to broad elements */
body:has(.sidebar) {
  /* styles */
}
:root:has(.content) {
  /* styles */
}
*:has(.item) {
  /* styles */
}

相反,应将 :has() 锚定到特定的元素,如 .container.gallery,以缩小范围并提高性能。

css
/* Use specific containers to limit scope */
.container:has(.sidebar-expanded) {
  /* styles */
}
.content-wrapper:has(> article[data-priority="high"]) {
  /* styles */
}
.gallery:has(> img[data-loaded="false"]) {
  /* styles */
}

最小化子树遍历

内部选择器(A:has(B) 中的 B)应该使用像 >+ 这样的组合器来限制遍历。当 :has() 内部的选择器没有被严格约束时,浏览器可能需要在每次 DOM 变更时遍历锚点元素的整个子树,以检查条件是否仍然成立。

在这个例子中,.ancestor 内部的任何变化都需要检查所有后代元素是否为 .foo

css
/* May trigger full subtree traversal */
.ancestor:has(.foo) {
  /* styles */
}

使用子代或兄弟组合器可以限制内部选择器的范围,从而降低 DOM 变更带来的性能成本。在这个例子中,浏览器只需要检查直接子元素或特定兄弟元素的后代。

css
/* More constrained - limits traversal */
.ancestor:has(> .foo) {
  /* direct child */
}
.ancestor:has(+ .sibling .foo) {
  /* descendant of adjacent sibling */
}

某些内部选择器可能会迫使浏览器在每次 DOM 变更时都向上遍历祖先链,以寻找可能需要更新的潜在锚点。当结构暗示需要检查变更元素的祖先时,就会发生这种情况。

在这个例子中,任何 DOM 变化都需要检查变更的元素是否为 .foo 的直接子元素(*),以及它的父元素(或更远的祖先)是否为 .ancestor

css
/* Might trigger ancestor traversal */
.ancestor:has(.foo > *) {
  /* styles */
}

通过使用特定的类或直接子代组合器(例如,下一个代码片段中的 .specific-child)来约束内部选择器,可以减少昂贵的祖先遍历,因为它将浏览器的检查限制在一个明确定义的元素上,从而提高性能。

css
/* Constrain the inner selector to avoid ancestor traversals */
.ancestor:has(.foo > .specific-child) {
  /* styles */
}

注意: 随着浏览器对 :has() 实现的优化,这些性能特征可能会得到改善,但基本的约束仍然存在::has() 需要遍历整个子树,因此你需要最小化子树的大小。在像 A:has(B) 这样的选择器中,确保你的 A 没有太多的子元素,并确保你的 B 受到严格约束,以避免不必要的遍历。

规范

规范
选择器 Level 4
# 关系型

浏览器兼容性

另见