CSS 选择器 :has() 完全指南

3 阅读7分钟

📋 概述

:has() 是 CSS 选择器 Level 4 引入的关系性伪类(relational pseudo-class),被社区称为"CSS 史上第一个父级选择器"。它允许我们根据元素的后代 / 兄弟 / 任意子元素是否匹配某些条件,来选中该元素本身。

在此之前,CSS 只能"向下"选择后代、或"向后"选择后续兄弟,却无法"向上"选择父级、也无法"向前"选择前置兄弟。:has() 补上了这块最后拼图,让纯 CSS 实现大量原本必须依赖 JavaScript 的交互效果成为可能。

/* 选中"包含 img 子元素"的 a 标签 */
a:has(img) {
    display: block;
}

/* 选中"其父元素内有 .active 兄弟"的卡片 */
.card:has(~ .active) { }

🔰 基础语法

1. 核心语法

<target>:has(<relative-selector-list>) {
    /* 当 target 内部(或相对位置)存在匹配 relative 的元素时生效 */
}
  • :has() 的参数是一个相对选择器列表(逗号分隔,任一匹配即成立,等价于"或")。
  • 相对选择器可以是任意合法选择器,包括后代、子代、兄弟、通用选择器,甚至可以嵌套 :has()
/* 多个条件:只要满足任意一个即匹配(OR 语义) */
section:has(h2, h3) {
    border-left: 3px solid #409eff;
}

/* 嵌套:选中"含有正在被 hover 的链接"的卡片 */
.card:has(a:hover) {
    box-shadow: 0 4px 16px rgba(0,0,0,.12);
}

⚠️ 语义注意:has(selector) 中的 selector 是相对于 :has() 前面的元素进行匹配的,不是相对于文档根。也就是说它检查的是"这个元素内部 / 周围是否存在符合条件的元素"。

2. 三种相对关系

:has() 的能力来自参数选择器的三种相对形态:

形态写法含义俗称
后代关系:has(img)元素内部任意层级有匹配元素父级选择器
子代关系:has(> .title)元素直接子元素有匹配元素严格父级
后续兄弟:has(~ .done)元素后面有同级匹配元素前置兄弟
前置兄弟:has(+ .error)元素紧邻其后有匹配元素相邻前置

🎯 核心用法

1. 真正的"父级选择器"

/* 包含图片链接的卡片,显示为块级并加内边距 */
a:has(img) {
    display: flex;
    gap: 12px;
    padding: 12px;
}

/* 含有无效输入(:invalid)的表单组,整组标红 */
.field:has(input:invalid) {
    border-color: #f56c6c;
}

2. 前置兄弟选择("前面有 X 的元素")

:has() 配合兄弟组合子可以选中"位于某个元素之前"的兄弟,这是以前 E + F / E ~ F 做不到的方向。

/* 选中 .step 中"后面跟着 .active"的所有步骤 —— 即实现"已完成步骤"高亮 */
.step:has(~ .active) {
    color: #67c23a;          /* 已完成 */
}

/* 表单里"紧挨着 .error 提示"的输入框上方标签变红 */
label:has(+ .error) {
    color: #f56c6c;
}

3. 根据子元素数量自适应布局

最经典的"空状态"与"数量自适应"场景:

/* 列表为空时显示提示 */
.list:has(> :not(*))::after {
    content: "暂无数据";
    color: #999;
}

/* 子项数量变化时切换布局方向 */
.grid:has(> :nth-child(5)) {
    grid-template-columns: repeat(5, 1fr);
}

/* 卡片内只有文字时收紧内边距(无图时) */
.card:not(:has(img)) {
    padding: 8px 12px;
}

4. 表单与无障碍联动

/* 勾选"同意"后,提交按钮才可点 */
form:has(input[type="checkbox"]:checked) button[type="submit"] {
    opacity: 1;
    pointer-events: auto;
}
form button[type="submit"] {
    opacity: .5;
    pointer-events: none;
}

/* 聚焦某输入框时,整张卡片高亮 */
.form-group:has(:focus-within) {
    border-color: #409eff;
    box-shadow: 0 0 0 3px rgba(64,158,255,.15);
}

5. 与 :is() / :not() / :where() 组合

/* 同时含有 img 和 video 的容器 */
.container:has(:is(img, video)) {
    aspect-ratio: 16 / 9;
}

/* 不含标题的区块加默认上间距 */
section:not(:has(h2, h3)) {
    margin-top: 24px;
}

💡 进阶技巧

1. 配合 :has() 实现"悬浮卡片"网格

.gallery:has(img:hover) img:not(:hover) {
    opacity: .6;          /* 鼠标悬停某张图时,其余图变暗 */
    filter: grayscale(1);
    transition: .3s;
}

2. 响应式"容器查询"的轻量替身

虽然 :has() 不等同于容器查询,但可以模拟简单的"内容驱动"样式:

.card:has(.badge) {
    /* 只要卡片里有徽标,整体用强调色描边 */
    border: 1px solid #e6a23c;
}

.card:has(.badge.hot) {
    /* 含有"热门"徽标时再升级 */
    border-color: #f56c6c;
    background: #fef0f0;
}

3. 选择器优先级(特异性)

:has() 本身的特异性由其参数列表中特异性最高的那个选择器决定(:is() 同理,:not() 也同理)。

/* :has(h2) 的特异性 = h2 的特异性(0,0,1) */
div:has(h2) {}        /* 特异性 (0,0,2) = div(1) + h2(1) */

/* 嵌套时取最高 */
:has(.a #b) {}        /* 取 #b 的特异性 (1,0,0) */

⚠️ 注意::has() 的"目标元素"(前面那个)特异性照常计入;参数只影响 :has() 这一环自身的权重。

4. 性能与渲染注意事项

  • :has()从右向左匹配后再"反向"影响父级,浏览器会对依赖它的元素建立额外的失效跟踪。
  • 避免在超深层、超大规模节点树的顶层(如 body:has(...))使用复杂参数,可能触发大范围重排。
  • 现代浏览器对 :has() 已做优化,正常规模下性能可接受;仍建议缩小作用范围(如限定在某个容器类下)而非全局使用。

🛠️ 实用场景

1. 卡片悬停联动(纯 CSS 灯箱预览)

.gallery:has(.thumb:hover) .preview {
    /* 任意缩略图被 hover,顶部预览区更新背景(配合不同自定义属性) */
}

2. 标题锚点高亮当前章节

/* 含锚点链接的标题,加书签图标 */
h2:has(a.anchor),
h3:has(a.anchor) {
    cursor: pointer;
}
h2:has(a.anchor)::before {
    content: "🔗";
    margin-right: 6px;
    opacity: .5;
}

3. 表格行"选中态"高亮(配合 checkbox)

tr:has(input[type="checkbox"]:checked) {
    background: #ecf5ff;
}

4. 文章"有代码块/图片"时调整阅读宽度

.article:has(pre) {
    /* 含代码块时给更宽的内容区 */
    --content-width: 860px;
}
.article:has(img) {
    --content-width: 720px;   /* 含图时稍窄,提升可读性 */
}

5. 表单"有部分填写"提示

form:has(input:not(:placeholder-shown)) .save-hint {
    display: block;           /* 有任意输入框填写过内容,显示"记得保存"提示 */
}

📊 浏览器支持

浏览器版本状态
Chrome105+✅ 2022-08 起支持
Edge105+
Safari15.4+✅(最早支持,含移动端)
Firefox121+✅ 2023-12 起支持
移动端 Safari (iOS)15.4+
移动端 Chrome (Android)105+

截至 2024 年,三大引擎(Blink / WebKit / Gecko)均已支持,可放心用于生产环境(无需再写大量 JS 兜底)。如需兼容老旧浏览器(如 Firefox < 121),可用 @supports 渐进增强:

@supports not selector(:has(*)) {
    /* 不支持 :has() 时的降级样式,或提示用户升级浏览器 */
    .card { border: 1px solid #ddd; }
}

⚠️ 注意事项与坑

  1. OR 语义,不是 AND

    /* ❌ 误区:以为同时含 a 和 b */
    .x:has(a, b)   /* 实际:含 a 或含 b 任意一个即可 */
    
    /* ✅ 想要"同时含 a 和 b",需要链式 :has() */
    .x:has(a):has(b) { }
    
  2. 不能直接选中"祖先的祖先之外"的任意元素 :has() 只能影响它前面的目标元素本身,不能越级改祖先的祖先。要影响更上层,需要把 :has() 写在更上层的选择器上。

  3. 避免无限 / 循环依赖 虽然 CSS 不会真正"死循环",但 :has(:has(...)) 嵌套过深会显著增加匹配成本,保持 1~2 层嵌套为宜。

  4. 与 JS 框架的 SSR / 虚拟 DOM 纯 CSS :has() 不影响 DOM 结构,配合 React / Vue 等框架渲染完全无副作用,是替代"为状态加 class"的优雅方案。

  5. 动画 / 过渡 :has() 触发的样式变化可以正常参与 transition / animation,体验与原生 class 切换一致。


🔗 相关资源


📝 总结

:has() 是 CSS 选择器的"终极补完",一句话记住它的能力边界:

  1. 它是关系性伪类,不是单纯的"父级选择器"——后代、子代、前后兄弟都能表达。
  2. 参数用 OR 语义,要 AND 就链式 :has(a):has(b)
  3. 特异性取参数里最高的那个,优先级计算时要留意。
  4. 三大浏览器均已支持,生产可用;旧浏览器用 @supports 降级。
  5. 大量"状态驱动样式"可从此告别 JS,让交互更轻、更声明式。

掌握 :has() 后,你会发现很多以前必须写 JS 联动的逻辑(表单联动、悬浮高亮、空状态、数量自适应布局)都能用一行 CSS 优雅解决。


创建时间:2026-09-17