先记住这个答案
:has() 被叫做「父选择器」是因为它能根据子元素的存在来选中父元素。比如 div:has(> img) 的意思是:如果这个 <div> 里面有直接子元素 <img>,就选中这个 <div>。以前 CSS 只能从上往下选,.parent .child 选的是 .child。:has() 第一次让你根据里面有什么来给外面加样式。
:has()根据后代或兄弟来选中元素本身div:has(> img)选含直接子图片的容器- 优先级和
:is()一样取参数最高值 - 不能嵌套使用,不能放在伪元素里
最常见的用法:根据内容给容器加样式
比如一个卡片组件,有图片的卡片和纯文字的卡片排版不同。以前只能在 HTML 上手动加类名区分,现在用 :has() 就能让 CSS 自己判断。
/* 普通卡片:纵向排列 */
.card {
display: flex;
flex-direction: column;
}
/* 有图片的卡片:横向排列 */
.card:has(> img) {
flex-direction: row;
}
/* 弹窗打开时禁止页面滚动 */
body:has(.modal-open) {
overflow: hidden;
}body:has(.modal-open) 是一个很实用的写法。以前打开弹窗时想给 <body> 加 overflow: hidden,只能用 JavaScript 操作类名。现在只要弹窗元素存在,:has() 就能自动生效。
不只是父选择器:还能选前面的兄弟
:has() 的参数可以用任意组合子。用 +(相邻兄弟)或 ~(通用兄弟)组合子时,它就变成了「前面的兄弟选择器」。
/* 后面紧跟段落的标题,加大底部间距 */
h2:has(+ p) {
margin-bottom: 24px;
}
/* 后面紧跟另一个标题的标题,缩小间距 */
h2:has(+ h2) {
margin-bottom: 8px;
}h2:has(+ p) 的意思是:这个 <h2> 后面紧跟着一个 <p> 时,选中这个 <h2>。以前 CSS 的 + 只能选后面的兄弟,现在配合 :has() 就能反过来选前面的。
优先级和限制
:has() 的优先级计算方式和 :is() 一样,取参数中优先级最高的那个选择器。:has(.a) 的优先级是一个类,:has(#b) 的优先级是一个 ID。
:has() 有两个硬限制。第一,不能嵌套::has(:has(.x)) 是不合法的。第二,不能放在伪元素里:::before:has(.x) 也不行。这些限制是为了避免循环依赖和性能问题。
另外要注意性能。:has() 是实时计算的,DOM 每次变化都会重新评估。在大列表上写 li:has(.active) 问题不大,但如果嵌套很深、选择器很复杂,浏览器需要做大量遍历。一般的场景不用担心,除非你在渲染数千个节点的列表。
容易答错的地方
- 以为 has() 只能选父元素
:has()的参数可以用>(子)、+(相邻兄弟)、~(通用兄弟)和空格(后代)。它不只是父选择器,还可以做前向兄弟选择器。- 以为 has() 能嵌套使用
:has(:has(.x))是不合法的,浏览器不会匹配。规范明确禁止在:has()的参数里再放:has()。
面试官还会怎么问?
怎么用 has() 做表单验证样式?
form:has(:invalid) 可以在表单中有任何一个字段校验失败时给整个表单加样式,比如显示红色边框或禁用提交按钮的视觉效果。不需要 JavaScript 监听每个字段的变化。
has() 的浏览器兼容性怎么样?
从 2023 年底开始,Chrome、Safari、Firefox 都已支持 :has()。但如果要兼容更早的浏览器版本,需要用 @supports selector(:has(.x)) 做特性检测,不支持的走降级样式。
has() 和 JavaScript 的 querySelector 里用法一样吗?
一样。document.querySelector('div:has(> img)') 在 JavaScript 里也能用,选中第一个包含直接子元素 <img> 的 <div>。:has() 是 CSS Selectors Level 4 的规范,CSS 和 JavaScript 的选择器 API 共享同一套语法。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。