学会 :has(),你就能用纯 CSS 实现过去必须写 JavaScript 才能做到的「根据子元素反选父元素」——下面从兼容性、语法到 5 个实战场景,一步步讲清楚它到底能干什么。
第一步:先确认环境支持
:has() 已在主流浏览器稳定落地:Chrome / Edge 105+、Safari 15.4+、Firefox 121+。2023 年底之后可以放心当增强特性用。
推荐把相关样式包在条件规则里:
@supports selector(:has(*)) {
/* :has() 的样式写在这里 */
}
注意:旧版浏览器会整条丢弃含 :has() 的规则(不会让页面崩),所以别让它承担布局主干,只当锦上添花。
第二步:记住三个语法要点
基本形式:A:has(B) —— 只要 A 内部存在匹配 B 的元素,就选中 A。
组合器可以随便带,这正是它强大的地方:
.card:has(> img) /* 直接子元素里有图片 */
h2:has(+ p) /* 后面紧跟段落的 h2(选中的是 h2) */
label:has(input:checked) /* 含已勾选输入框的 label */
特异性规则::has() 取参数中最高的特异性。a:has(.x) 的特异性 = 一个元素选择器 + 一个类。
注意::has() 不能嵌套(:has(:has(...)) 无效),参数里不能写伪元素(::before),:has() 本身也不能挂在伪元素后面(::before:has() 无效)。这三条是最常见的「写了没反应」。
第三步:五个马上能用的场景
1. 表单校验时高亮整块区域
.fieldset:has(input:invalid:not(:placeholder-shown)) {
border-color: #e5484d;
}
不用给每个 input 绑 blur 事件加类名,浏览器自动算。
2. 有图 / 无图的卡片自动换布局
.card:has(> img) { grid-template-columns: 120px 1fr; }
.card:not(:has(> img)) { grid-template-columns: 1fr; }
3. 条目数量驱动列数
.list:has(> :nth-child(4)) { grid-template-columns: repeat(2, 1fr); }
.list:has(> :nth-child(7)) { grid-template-columns: repeat(3, 1fr); }
4. 纯 CSS 主题切换(零 JS)
body:has(#theme-dark:checked) { --bg: #111; --fg: #eee; }
5. 空状态提示
.todo:has(> li:not(.done)) .empty-tip { display: none; }
还有未完成项时隐藏「暂无任务」提示。
第四步:注意性能
:has() 的代价是样式失效范围可能变大——子元素一变,浏览器要回头检查祖先。
- 不要写
*:has(...)、body:has(...) 这种从根节点撒网的写法;
- 把范围收紧到类名,如
.comment-list:has(> .pinned);
- 参数优先用直接子组合器
>,比后代匹配便宜;
- 别在
:has() 里用 :hover、:focus 这类高频动态伪类去触发大范围重排。
注意:如果输入、滚动时掉帧,先在 DevTools 的 Performance 面板确认是不是 :has() 引起的大范围样式重算,再考虑退回 JS 加类。
第五步:排查三步
- 选择器是否命中?在 Elements 面板按
Ctrl+F 搜 :has(...),能高亮就是命中了。
- 是不是特异性冲突?Console 里执行
element.matches(':has(...)') 验证。
- 浏览器支持吗?
CSS.supports('selector(:has(*))') 返回 true 才生效。
小结
:has() 让 CSS 第一次具备反向选择能力,可替代大量「加类名」的 JS 逻辑:表单校验、有图无图布局、数量驱动布局、主题切换、空状态。
- 语法核心是
A:has(B) 加组合器;特异性取参数最高值;不能嵌套、不能配伪元素。
- 用
@supports selector(:has(*)) 做渐进增强,关键功能保留 JS 兜底。
- 性能靠「收窄范围 + 优先用
>」,别在根节点上撒网。