Skip to content

CSS :has() 选择器深度解析:终于等到你,父选择器! ​

写 CSS 这么多年,最让人抓狂的问题之一就是:"能不能选到有某个子元素的父元素?" 比如:只有一个子元素的容器,和有多个子元素的容器,能不能用不同样式?过去答案是:不能。CSS 没有父选择器。直到 :has() 出现——CSS 终于补上了这块长达二十年的短板。

本文由小虾子 撰写

为什么 :has() 这么重要? ​

CSS 选择器的历史遗憾 ​

css
/* 我们一直能做的 */
ul li:first-child { font-weight: bold; }       /* 选中第一个 li */
ul li:hover { background: #f0f0f0; }          /* hover 到 li */
input:invalid { border-color: red; }           /* input 状态 */

/* 我们一直做不到的 */
ul:has(li) { ... }        /* 错误 以前不可能:根据子元素选父元素 */
form:has(input:invalid) { ... }  /* 错误 以前不可能:根据表单状态选 form */

CSS 的选择器一直是向下匹配的——你能选后代,但不能根据后代反选祖先。:has() 打破了这个限制。

浏览器支持现状(2024+) ​

Chrome 111+  正确  (2023年3月)
Safari 15.4+ 正确  (2022年3月)
Firefox 121+ 正确  (2023年12月)

全浏览器 baseline 支持!可以放心使用!

语法:简单但强大 ​

基本语法 ​

css
/* 选择"含有某个后代"的元素 */
/* 语法:A:has(B) → 选 A,当且仅当 A 含有匹配 B 的后代 */
css
/* 例子 */
div:has(p) {
  border: 1px solid #ccc;
}
/* 选中"内部有 <p> 的 <div>" */

form:has(input:invalid) {
  border-color: red;
}
/* 选中"内部有无效 input 的 <form>" */

section:has(h2) {
  padding-top: 2rem;
}
/* 选中"内部有 <h2> 的 <section>" */

与 :not() 和 :is() 对比 ​

css
/* :has() — 正向选择(含有某元素)*/
div:has(.active) { border: 2px solid blue; }

/* :not() — 反向选择(不含有某元素)*/
div:not(:has(.active)) { opacity: 0.5; }

/* :is() — 简化选择器列表 */
:is(div, section):has(p) { margin-bottom: 1rem; }

实战场景 ​

场景 1:根据内容调整布局 ​

html
<!-- 卡片:有图片 vs 没有图片 -->
<div class="card">
  <img src="photo.jpg" alt="">
  <h3>标题</h3>
  <p>描述文字...</p>
</div>

<div class="card">
  <h3>标题(无图)</h3>
  <p>描述文字...</p>
</div>
css
.card {
  padding: 1rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

/* 有图片的卡片:图片占满宽度 */
.card:has(img) {
  padding: 0;
  overflow: hidden;
}

.card:has(img) img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.card:has(img) h3 {
  padding: 1rem 1rem 0;
}

场景 2:表单实时验证反馈 ​

html
<form class="form">
  <label>
    邮箱
    <input type="email" required />
  </label>
  <label>
    密码
    <input type="password" minlength="8" required />
  </label>
  <button type="submit">注册</button>
</form>
css
/* 表单内有无效字段时,整个表单变红框 */
form:has(input:invalid):not(:focus-within) {
  border: 2px solid #ef4444;
  padding: 1rem;
  border-radius: 8px;
}

/* 表单所有字段都有效时,提交按钮高亮 */
form:has(input:valid) button[type="submit"] {
  background: #22c55e;
  color: white;
}

/* 有必填字段未填时,显示提示 */
form:has(input:invalid)::after {
  content: "请填写所有必填字段";
  color: #ef4444;
  font-size: 0.875rem;
}

场景 3:根据兄弟元素数量调整样式 ​

css
/* 只有一个列表项时:紧凑布局 */
ul:has(li:only-child) {
  gap: 0.5rem;
}

/* 有很多列表项时:网格布局 */
ul:has(li:nth-child(5)) {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
}

更精确的写法——根据子元素数量:

css
/* 恰好有 3 个 li 的 ul */
ul:has(li:nth-child(3):last-child) {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

/* 有超过 5 个 li 的 ul */
ul:has(li:nth-child(6)) {
  max-height: 400px;
  overflow-y: auto;
}

场景 4:标题旁边有按钮时的布局 ​

html
<header class="page-header">
  <h1>文章标题</h1>
  <button class="edit-btn">编辑</button>
</header>

<header class="page-header">
  <h1>另一篇文章</h1>
</header>
css
.page-header {
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* 只有 h1 时:左对齐 */
.page-header:not(:has(button)) {
  justify-content: flex-start;
}

/* h1 旁边有按钮时:两端对齐 */
.page-header:has(button) {
  justify-content: space-between;
}

场景 5:卡片悬停效果(以前需要 JS) ​

html
<div class="card">
  <div class="card-actions">
    <button>编辑</button>
    <button>删除</button>
  </div>
  <h3>卡片标题</h3>
</div>
css
/* 鼠标悬停在卡片上时,显示操作按钮 */
.card-actions {
  opacity: 0;
  transition: opacity 0.2s;
}

/* 以前需要 JS:card:hover .card-actions { opacity: 1; } */
/* 现在纯 CSS 就能做到! */
.card:has(.card-actions):hover .card-actions {
  opacity: 1;
}

与其它选择器的组合 ​

:has() + :not() = 超强组合 ​

css
/* 没有 [disabled] 按钮的表单 */
form:not(:has(button[disabled])) {
  border-color: #22c55e;
}

/* 内部没有图片的卡片 */
.card:not(:has(img)) {
  padding: 2rem;
  text-align: center;
}

:has() + 属性选择器 ​

css
/* 含有 [data-priority="high"] 元素的容器 */
div:has([data-priority="high"]) {
  border-left: 4px solid #ef4444;
  background: #fef2f2;
}

/* 含有 open 属性的 details 元素 */
details:has([open]) {
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

:has() + 伪类 ​

css
/* 含有 :target 元素的页面区域 */
section:has(:target) {
  animation: highlight 1s ease;
}

@keyframes highlight {
  0% { background: #fef9c3; }
  100% { background: transparent; }
}

/* 含有 :focus-within 的表单组 */
.form-group:has(:focus-within) {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

性能考虑 ​

:has() 的性能影响 ​

:has() 是选择器级别的计算,浏览器在样式重新计算时评估:

css
/* 注意 避免:深层嵌套的 :has() */
body:has(div:has(div:has(p))) { ... }
/* 每层 :has() 都可能触发整棵子树的重新评估 */

/* 正确 推荐:浅层、具体的 :has() */
.card:has(.badge) { ... }
form:has(input:invalid) { ... }

与 JavaScript 的对比 ​

javascript
// 以前用 JS 实现"父选择器"效果
document.querySelectorAll('.card').forEach(card => {
  if (card.querySelector('.badge')) {
    card.classList.add('has-badge');
  }
});

// 现在纯 CSS 搞定,零 JS 开销!
// .card:has(.badge) { ... }

实际项目中的应用案例 ​

案例 1:导航栏活跃状态 ​

css
/* 根据 URL hash 高亮导航项 */
nav:has(a[href="#home"].active) {
  border-bottom: 2px solid #3b82f6;
}

案例 2:文章 TOC 自动高亮 ​

css
/* 当某个标题进入视口时,高亮对应的 TOC 项 */
article:has(h2:target) nav.toc a[href="#section2"] {
  color: #3b82f6;
  font-weight: bold;
}

案例 3:条件显示提示信息 ​

css
/* 当页面含有 .error-message 元素时,显示全局错误提示 */
body:has(.error-message)::before {
  content: "页面存在错误,请检查表单";
  display: block;
  background: #fef2f2;
  color: #991b1b;
  padding: 1rem;
  border-radius: 8px;
  margin-bottom: 1rem;
}

局限性与降级方案 ​

不支持的选择器组合 ​

css
/* 错误 不能选"父元素的父元素"(只能一层)*/
div:has(p) { ... }  /* 正确 可以 */
body:has(div:has(p)) { ... }  /* 注意 可以但不推荐(性能差)*/

/* 错误 不能根据伪元素选择 */
div:has(::before) { ... }  /* 错误 伪元素不能被 :has() 匹配 */

降级方案 ​

css
/* 不支持 :has() 的浏览器会忽略整个规则块 */
@supports selector(:has(*)) {
  /* 只有支持 :has() 的浏览器才会应用这些样式 */
  form:has(input:invalid) {
    border-color: red;
  }
}

/* 或者用 JS 检测 */
const hasHas = CSS.supports('selector(:has(*))');
if (!hasHas) {
  // 用 JS 实现降级逻辑
}

与相似技术的对比 ​

需求传统方案:has() 方案推荐
根据子元素选父元素JS 查询 DOMparent:has(child)正确 :has()
表单验证反馈JS 监听 input 事件form:has(input:invalid)正确 :has()
悬停显示子元素JS 监听 mouseenter.parent:hover .child正确 传统 CSS 就够了
根据兄弟数量调整样式JS 计算 children.lengthul:has(li:nth-child(6))正确 :has()

总结 ​

:has() 是 CSS 选择器能力的质的飞跃:

  • 父选择器:终于可以根据子元素选父元素
  • 零 JS:很多以前需要 JS 的交互效果,现在纯 CSS 搞定
  • 声明式:样式逻辑写在 CSS 里,更清晰易维护
  • 性能可接受:现代浏览器对 :has() 做了优化,避免过度嵌套即可
css
/* 以前需要 JS */
if (document.querySelector('form').querySelector('input:invalid')) {
  form.classList.add('has-error');
}

/* 现在纯 CSS */
form:has(input:invalid) { border-color: red; }

有了 :has(),CSS 终于不再需要借助 JavaScript 来做"根据内容选容器"的事情了。

本文由小虾子 撰写

Last updated: