目录

CSS :has() 父级选择器与关系查询

嗨,我是芦苇Z 👋。

在前端开发中,我们经常会遇到以下需求:

  • 父元素样式依赖子元素状态 —— 如导航栏中,父级 <li> 需根据子 <a> 是否 .active 来高亮。
  • 布局随内容变化 —— 比如 CSS Grid 容器在子元素数量超过阈值时切换列数。
  • 交互驱动父级样式 —— 表单验证时,父级 <form> 根据内部 <input> 的合法性改变状态。

过去,这类场景往往只能靠 JavaScript:通过 DOM 遍历查询子元素,再手动给父元素加 class。这样不仅增加代码量,也让逻辑分散在 CSS 与 JS 之间,降低可维护性。

现在,主流浏览器都已支持 CSS :has() 伪类选择器 。它赋予了 CSS “向上选择”“关系查询” 的能力,让许多原本必须写脚本的逻辑,直接在样式表中完成。

:has() 源于 W3C Selectors Level 4 规范,是继 :is():not() 等条件选择器之后的重要增强。

在传统 CSS 中,选择器是单向的:只能自上而下匹配(父 → 子),无法根据子元素的状态选中父元素,这被称为 Parent Selector Problem(父选择器难题)

随着浏览器性能和选择器引擎优化,现代浏览器已支持 :has()

  • Chrome ≥ 105
  • Safari ≥ 15.4
  • Edge ≥ 105
  • Firefox ≥ 121

因此,:has() 的出现不仅是语法扩展,更是 CSS 从“静态描述”走向“关系驱动”的关键转折

:has() 的基本用法是:根据子元素或兄弟元素的条件,选择其父级或当前元素

/* 父 li 包含直接子元素 .active */
li:has(> .active)

/* 紧跟着 h2 的 h1 */
h1:has(+ h2)

/* 包含无效必填 input 的 form */
form:has(input[required]:invalid)

核心规则:

  • 选择主体:始终是 :has() 左侧的元素
  • 关系参数:括号内的选择器用于设定条件,不合法的会被忽略。
  • 特异性:由 :has() 内选择器的最高特异性决定,与 :is():not() 一致。

:has() 内几乎可以接收任意复合选择器,从而实现灵活匹配。

figure:has(img)          /* 包含 <img> 的 figure */
article:has(#featured)   /* 内部含有 id="featured" 的 article */
li:has(a[target="_blank"])
div:has(a[href*="toulan.fun"])
form:has(:focus-visible)
label:has(input:checked)
section:has(> h2:first-child)

严格意义上,CSS 选择器无法直接匹配“文本内容”。但部分场景可通过 :contains()(部分浏览器实验特性或第三方扩展)、[attr*="text"] 等间接实现。

例如:

/* 仅在支持 :contains() 的环境下可用 */
a:has(:contains("立即购买"))

/* 按 aria-label 模拟文本匹配 */
button:has([aria-label*="提交"])

⚠️ 注意:原生 CSS 尚未正式支持基于纯文本内容的匹配,常见做法是利用 属性值JS 辅助

用途示例代码
父元素依赖子状态.menu li:has(> a.active)
紧邻关系匹配h1:has(+ h2)
表单校验form:has(input[required]:invalid)
按属性选择div:has(a[target="_blank"])
按状态选择label:has(input:checked)
全局控制html:has(dialog[open])
.menu li:has(> a.active) {
  background-color: whitesmoke;
}
.grid:has(.grid-item:nth-child(7)) {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
html:has(dialog[open]) {
  overflow: hidden; /* 打开对话框时禁止滚动 */
}
// 传统方式
const checkedLabels = Array.from(document.querySelectorAll('label'))
  .filter(label => label.querySelector('input:checked'));

// 使用 :has()
const checkedLabels = document.querySelectorAll('label:has(input:checked)');
const available = page.locator(
  '.product-card:has(button.add-to-cart):not(:has(.out-of-stock-label))'
);
await available.first().click();
/* 隐藏包含广告链接的 div */
div:has(a[href*="doubleclick.net"]) {
  display: none !important;
}

/* 隐藏没有核心内容的 section */
main section:not(:has(p, img, h2, ul)) {
  display: none;
}

尽管 :has() 十分强大,但其匹配开销较大。使用时建议:

  1. 限定作用域:尽量基于具体容器使用,而非全局通配。

    /* ✅ 推荐 */
    .nav li:has(> a.active)
    
    /* ❌ 慎用 */
    :has(.active)
  2. 优先明确关系:用 >+~ 限制范围,避免深层后代选择器。

  3. 保持条件简洁:过度组合选择器会增加渲染压力。

  4. 注意特异性:避免因 :has() 中选择器特异性过高,导致样式难以覆盖。

CSS :has() 为选择器带来了 父级选择关系查询 的能力,突破了长期以来的单向限制。

它在以下方面尤其有价值:

  • CSS 样式控制 —— 父元素随子状态动态变化。
  • JavaScript DOM 查询 —— 语义更简洁,代码更易维护。
  • 自动化测试与爬虫 —— 复杂场景下的精准定位。
  • 浏览器扩展与内容过滤 —— 高效表达内容匹配逻辑。

作为前端开发者,应将 :has() 纳入现代工具箱,合理使用它来简化逻辑、提升可维护性,但也需时刻关注 性能与兼容性