📋 概述
: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; /* 有任意输入框填写过内容,显示"记得保存"提示 */
}
📊 浏览器支持
| 浏览器 | 版本 | 状态 |
|---|---|---|
| Chrome | 105+ | ✅ 2022-08 起支持 |
| Edge | 105+ | ✅ |
| Safari | 15.4+ | ✅(最早支持,含移动端) |
| Firefox | 121+ | ✅ 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; }
}
⚠️ 注意事项与坑
-
OR 语义,不是 AND
/* ❌ 误区:以为同时含 a 和 b */ .x:has(a, b) /* 实际:含 a 或含 b 任意一个即可 */ /* ✅ 想要"同时含 a 和 b",需要链式 :has() */ .x:has(a):has(b) { } -
不能直接选中"祖先的祖先之外"的任意元素
:has()只能影响它前面的目标元素本身,不能越级改祖先的祖先。要影响更上层,需要把:has()写在更上层的选择器上。 -
避免无限 / 循环依赖 虽然 CSS 不会真正"死循环",但
:has(:has(...))嵌套过深会显著增加匹配成本,保持 1~2 层嵌套为宜。 -
与 JS 框架的 SSR / 虚拟 DOM 纯 CSS
:has()不影响 DOM 结构,配合 React / Vue 等框架渲染完全无副作用,是替代"为状态加 class"的优雅方案。 -
动画 / 过渡
:has()触发的样式变化可以正常参与transition/animation,体验与原生 class 切换一致。
🔗 相关资源
📝 总结
:has() 是 CSS 选择器的"终极补完",一句话记住它的能力边界:
- 它是关系性伪类,不是单纯的"父级选择器"——后代、子代、前后兄弟都能表达。
- 参数用 OR 语义,要 AND 就链式
:has(a):has(b)。 - 特异性取参数里最高的那个,优先级计算时要留意。
- 三大浏览器均已支持,生产可用;旧浏览器用
@supports降级。 - 大量"状态驱动样式"可从此告别 JS,让交互更轻、更声明式。
掌握 :has() 后,你会发现很多以前必须写 JS 联动的逻辑(表单联动、悬浮高亮、空状态、数量自适应布局)都能用一行 CSS 优雅解决。
创建时间:2026-09-17