:has()
功能性 :has() 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(<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> 元素设置样式。
<section>
<article class="featured">Featured content</article>
<article>Regular content</article>
</section>
<section>
<article>Regular content</article>
</section>
section:has(.featured) {
border: 2px solid blue;
}
结果
与兄弟组合器一起使用
下面例子中的 :has() 样式声明调整了 <h1> 标题后的间距,条件是它后面紧跟着一个 <h2> 标题。
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
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
<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
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() 伪类帮助选择了任何后面紧跟着(由 + 指示)一个 H2、H3 或 H4 元素的 H1、H2 或 H3 元素,并且 CSS 规则减小了这些 H1、H2 或 H3 元素后面的间距。
这个选择器也可以写成
:is(h1, h2, h3):has(+ h2, + h3, + h4) {
margin: 0 0 0.25rem 0;
}
逻辑运算
:has() 关系选择器可以用来检查多个特性中是否有一个为真,或者是否所有特性都为真。
通过在 :has() 关系选择器中使用逗号分隔的值,你正在检查是否存在任何一个参数。x:has(a, b) 会在后代 a 或 b 存在时为 x 设置样式。
通过将多个 :has() 关系选择器链接在一起,你正在检查是否所有参数都存在。x:has(a):has(b) 会在后代 a 和 b 都存在时为 x 设置样式。
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() 条件。
/* Avoid anchoring :has() to broad elements */
body:has(.sidebar) {
/* styles */
}
:root:has(.content) {
/* styles */
}
*:has(.item) {
/* styles */
}
相反,应将 :has() 锚定到特定的元素,如 .container 或 .gallery,以缩小范围并提高性能。
/* 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。
/* May trigger full subtree traversal */
.ancestor:has(.foo) {
/* styles */
}
使用子代或兄弟组合器可以限制内部选择器的范围,从而降低 DOM 变更带来的性能成本。在这个例子中,浏览器只需要检查直接子元素或特定兄弟元素的后代。
/* More constrained - limits traversal */
.ancestor:has(> .foo) {
/* direct child */
}
.ancestor:has(+ .sibling .foo) {
/* descendant of adjacent sibling */
}
某些内部选择器可能会迫使浏览器在每次 DOM 变更时都向上遍历祖先链,以寻找可能需要更新的潜在锚点。当结构暗示需要检查变更元素的祖先时,就会发生这种情况。
在这个例子中,任何 DOM 变化都需要检查变更的元素是否为 .foo 的直接子元素(*),以及它的父元素(或更远的祖先)是否为 .ancestor。
/* Might trigger ancestor traversal */
.ancestor:has(.foo > *) {
/* styles */
}
通过使用特定的类或直接子代组合器(例如,下一个代码片段中的 .specific-child)来约束内部选择器,可以减少昂贵的祖先遍历,因为它将浏览器的检查限制在一个明确定义的元素上,从而提高性能。
/* Constrain the inner selector to avoid ancestor traversals */
.ancestor:has(.foo > .specific-child) {
/* styles */
}
注意: 随着浏览器对 :has() 实现的优化,这些性能特征可能会得到改善,但基本的约束仍然存在::has() 需要遍历整个子树,因此你需要最小化子树的大小。在像 A:has(B) 这样的选择器中,确保你的 A 没有太多的子元素,并确保你的 B 受到严格约束,以避免不必要的遍历。
规范
| 规范 |
|---|
| 选择器 Level 4 # 关系型 |
浏览器兼容性
加载中…