CSS2025-03-20
返回首页

CSS `:has()` 选择器:父选择器终于来了

CSS :has() 选择器:父选择器终于来了

CSS :has() 选择器在 2023 年底拿到主流浏览器支持,到 2025 年已经可以放心使用了。它被称作"CSS 的父选择器"——虽然不只是选父元素,但这是它最常见的用法。

基本语法

:has() 的用法是根据后代元素的状态来匹配祖先元素:

/* 选中"包含 img 子元素的 figure" */
figure:has(img) {
  border: 1px solid #ddd;
  padding: 1rem;
}

/* 选中"包含 checked 的 checkbox 的 label" */
label:has(input:checked) {
  font-weight: bold;
  color: green;
}

/* 选中"包含 placeholder 的输入框的父容器" */
.form-group:has(input:placeholder-shown) {
  /* 输入框还空着时的样式 */
}

实际使用场景

1. 表单验证

以前做表单验证状态样式需要 JavaScript 添加 class。现在纯 CSS 就能搞定:

/* 输入框获得焦点且有值时的样式 */
.form-item:has(input:focus:not(:placeholder-shown)) {
  border-color: #10b981;
}

/* 输入框有值且失焦时的样式 */
.form-item:has(input:not(:placeholder-shown):not(:focus)) {
  border-color: #10b981;
}

/* 输入框为空且失焦时的样式(需要必填) */
.form-item:has(input:required:placeholder-shown:not(:focus)) {
  border-color: #ef4444;
}

placeholder-shown 是关键——输入框有值时 placeholder 不显示,没有值时显示。用这个伪类来判断输入框是否为空,不需要 JS。

2. 响应式组件

根据子元素的数量调整布局:

/* 子元素 <= 3 个时水平排列 */
.card-list:has(> *:nth-child(-n+3):last-child) {
  display: flex;
  gap: 1rem;
}

/* 子元素 > 3 个时网格布局 */
.card-list:has(> *:nth-child(4)) {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

3. 暗色模式头像适配

根据背景色自动调整头像边框颜色:

/* 头像在有深色背景的容器里 */
.dark-section:has(.avatar) .avatar {
  border-color: rgba(255, 255, 255, 0.3);
}

4. 空状态

检测容器是否为空:

/* 容器没有子元素时显示空状态 */
.list-container:not(:has(> *))::after {
  content: '暂无数据';
  display: block;
  text-align: center;
  padding: 2rem;
  color: #999;
}

/* 容器有子元素时隐藏空状态 */
.list-container:has(> *)::after {
  display: none;
}

5. 悬停父元素高亮子元素

/* 鼠标悬停在表格行上时,整行高亮 */
tr:hover td {
  background: #f0f9ff;
}

/* 更灵活的方式:鼠标在行内任意位置都高亮 */
.table-row:has(td:hover) td {
  background: #f0f9ff;
}

和 JavaScript 的配合

:has() 减少了大量 DOM 操作的场景。以前需要 JS 添加/移除 class 的情况,现在可以用 CSS 直接处理。

// 以前
const card = document.querySelector('.card');
const img = card.querySelector('img');
if (img) {
  card.classList.add('has-image');
}

// 现在:不需要 JS

这意味着更少的 DOM 操作,更好的性能,更少的代码。

性能考虑

:has() 的性能曾经是大家担心的点。2025 年主流浏览器的实现已经很成熟了,性能不是问题。但有几个注意事项:

  1. 避免过度嵌套:has() 内部不要嵌套太深
  2. 避免通配符:has(*) 会匹配所有后代,性能差
  3. 避免在动画中使用:has() 的计算可能触发重排
/* 好:明确指定子元素 */
.card:has(img) { ... }

/* 差:通配符 */
.card:has(*) { ... }

/* 差:嵌套太深 */
.container:has(.item:has(.sub-item:has(.deep-item))) { ... }

浏览器兼容性

到 2025 年,所有主流浏览器都支持了 :has()

  • Chrome 105+
  • Safari 15.4+
  • Firefox 121+

Firefox 在 2023 年底才支持,是目前需要注意兼容性的最后一个浏览器。如果需要支持旧版 Firefox,用 @supports 做渐进增强:

/* 不支持 :has() 时的降级方案 */
.card {
  border: 1px solid #ddd;
  padding: 1rem;
}

/* 支持 :has() 时的增强 */
@supports selector(:has(*)) {
  .card:not(:has(img)) {
    border: none;
    padding: 0;
  }
}

写在最后

:has() 是 CSS 近年来最有用的选择器之一。它让很多以前需要 JavaScript 才能实现的交互变成了纯 CSS 方案,减少了 JS 和 CSS 的耦合。项目里大量用上之后,代码确实更干净了。