一、为什么我们需要 :has()?

在 :has() 出现之前,CSS 一直有一个著名的限制:没有真正的父选择器。我们无法根据子元素的状态来改变父元素的样式,这导致很多交互效果必须借助 JavaScript 来实现。

例如,当一个表单输入框获得焦点时,我们想要高亮整个表单容器;或者当一个列表中包含特定子元素时,需要调整列表的布局。这些场景在传统 CSS 中几乎无法纯 CSS 实现。

:has() 选择器的诞生彻底改变了这一局面,它被称为"父选择器",但实际上它的能力远不止于此——它是一个关系选择器,可以根据后代或兄弟元素的状态来匹配元素。

二、浏览器支持现状

:has() 选择器的浏览器支持已经达到非常好的水平:

  • Chrome / Edge:105+(2022年9月)
  • Safari:15.4+(2022年3月)
  • Firefox:121+(2023年12月)
  • 移动端:iOS Safari 15.4+、Chrome Android 105+

截至2026年,全球覆盖率已超过 95%,可以安全用于生产环境。对于需要兼容旧浏览器的项目,建议配合 @supports 进行渐进增强。

三、核心语法解析

:has() 的基本语法非常直观:

/* 基础语法:选择包含特定子元素的父元素 */
父元素:has(子选择器) {
  /* 样式规则 */
}

/* 示例:当卡片包含图片时,改变布局方向 */
.card:has(img) {
  flex-direction: row;
}

/* 示例:当输入框获得焦点时,高亮表单区域 */
form:has(input:focus) {
  border-color: #4A90E2;
  box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.15);
}

/* 示例:当列表中有选中项时,显示批量操作栏 */
.todo-list:has(.item.selected) {
  border-top: 2px solid #4A90E2;
}

需要注意的是,:has() 中的选择器可以使用相对选择器语法(> 子选择器、+ 相邻兄弟、~ 通用兄弟),这使得它的表达能力非常强大。

四、:has() 与 :not() 的组合魔法

:has() 与 :not() 的组合使用可以创造出极其精妙的样式规则:

/* 选择不包含图片的卡片 */
.card:not(:has(img)) {
  padding: 24px;
}

/* 选择至少包含3个子元素的容器 */
.container:has(> :nth-child(3)) {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

/* 选择没有空列表项的列表 */
.list:not(:has(.empty-state)) {
  min-height: auto;
}

/* 选择包含无效输入的表单区域 */
.form-group:has(input:invalid) {
  border-left: 3px solid #E74C3C;
}

五、实战场景:纯 CSS 表单验证反馈

这是 :has() 最经典的应用场景之一——无需 JavaScript 即可实现即时表单验证反馈:

/* 表单区域基础样式 */
.form-group {
  padding: 12px 16px;
  border-left: 3px solid transparent;
  border-radius: 8px;
  transition: all 0.3s ease;
}

/* 当输入框获得焦点时高亮 */
.form-group:has(input:focus) {
  background-color: #F8FAFE;
  border-left-color: #4A90E2;
}

/* 当输入无效时显示错误状态(且不是placeholder显示状态) */
.form-group:has(input:invalid:not(:placeholder-shown)) {
  border-left-color: #E74C3C;
}

/* 当输入有效时显示成功状态 */
.form-group:has(input:valid) {
  border-left-color: #2ECC71;
}

/* 错误提示文字 */
.error-message {
  display: none;
  color: #E74C3C;
  font-size: 13px;
  margin-top: 4px;
}

/* 仅当有无效输入时显示错误消息 */
.form-group:has(input:invalid:not(:placeholder-shown)) .error-message {
  display: block;
}

六、实战场景:响应式卡片布局

结合容器查询,:has() 可以创造出智能的卡片布局:

/* 卡片基础样式 */
.card {
  container-type: inline-size;
  container-name: card;
  padding: 16px;
  border-radius: 12px;
  background: white;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}

/* 当卡片包含特色标签时,添加顶部装饰条 */
.card:has(.badge-featured) {
  border-top: 4px solid gold;
  transform: translateY(-2px);
}

/* 当卡片包含多个操作按钮时,调整底部布局 */
.card:has(.actions > :nth-child(2)) .actions {
  justify-content: space-between;
}

/* 当卡片中有图片且为横向布局时 */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 16px;
}

/* 小容器内的卡片布局调整 */
@container card (max-width: 400px) {
  .card:has(img) {
    grid-template-columns: 1fr;
  }
  
  /* 狭小空间下,如果有徽章,调整位置 */
  .card:has(.badge) .badge {
    position: absolute;
    top: 8px;
    right: 8px;
  }
}

七、实战场景:导航菜单高亮

根据当前页面高亮对应导航项,无需后端添加 active 类:

/* 导航栏基础样式 */
.nav {
  display: flex;
  gap: 8px;
  padding: 12px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}

.nav-item {
  padding: 8px 16px;
  border-radius: 8px;
  text-decoration: none;
  color: #555;
  transition: all 0.2s ease;
}

/* 当前页面链接高亮 */
.nav-item[aria-current="page"] {
  background: #4A90E2;
  color: white;
}

/* 当导航栏中有当前项时,调整整体布局 */
.nav:has([aria-current="page"]) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

/* 当某个菜单项有子菜单时显示指示器 */
.nav-item:has(.dropdown)::after {
  content: "▾";
  margin-left: 4px;
}

/* 下拉菜单展开 */
.nav-item:has(.dropdown):hover .dropdown {
  display: block;
}

八、实战场景:动态表格样式

根据表格内容自动应用不同样式,提升数据可读性:

/* 表格基础样式 */
.data-table {
  width: 100%;
  border-collapse: collapse;
}

/* 表格行基础样式 */
.data-table tr {
  border-bottom: 1px solid #eee;
  transition: background 0.2s;
}

/* 当行包含高优先级标记时 */
.data-table tr:has(.priority-high) {
  background-color: #FFF5F5;
  border-left: 3px solid #E74C3C;
}

/* 当行包含完成标记时 */
.data-table tr:has(.status-done) {
  opacity: 0.6;
}

/* 当表格包含选中行时,显示批量操作栏 */
.table-wrapper:has(.data-table tr:has(input:checked)) .bulk-actions {
  opacity: 1;
  transform: translateY(0);
}

.bulk-actions {
  opacity: 0;
  transform: translateY(-10px);
  transition: all 0.3s ease;
  padding: 12px;
  background: #F0F7FF;
  border-radius: 8px;
  margin-bottom: 12px;
}

/* 当表格为空时显示空状态 */
.table-wrapper:has(.data-table tbody:empty) .empty-state {
  display: flex;
}

/* 当列包含数字时右对齐 */
.data-table td[data-type="number"] {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

九、性能考量与最佳实践

虽然 :has() 非常强大,但不当使用可能带来性能问题:

性能陷阱:

  • 避免 * 通配符*:has(.foo) 会匹配大量元素,严重影响性能
  • 避免过度嵌套:has(:has(:has(...))) 会导致计算复杂度指数级增长
  • 谨慎使用后代选择器.container:has span 需要遍历所有后代节点

最佳实践:

/* 不推荐:范围太广 */
.container:has(input:focus) { ... }

/* 推荐:使用子选择器限定范围 */
.form-group:has(> input:focus) { ... }

/* 不推荐:过度嵌套 */
.card:has(.content:has(.title:has(span))) { ... }

/* 推荐:扁平化选择器 */
.card:has(.title-badge) { ... }

/* 推荐:结合 class 使用 */
.card:has(.badge) { ... }
/* 而不是 */
.card:has([data-badge]) { ... } /* 属性选择器性能稍差 */

渐进增强策略:

/* 基础样式(所有浏览器) */
.card {
  padding: 16px;
  border: 1px solid #ddd;
}

/* 增强样式(支持 :has() 的浏览器) */
@supports selector(:has(*)) {
  .card:has(img) {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
  
  .form-group:has(input:invalid) {
    border-color: #E74C3C;
  }
}

十、:has() vs JavaScript 对比

以下是常见场景下 :has() 替代 JavaScript 的方案对比:

场景JavaScript 方案CSS :has() 方案
输入框聚焦高亮父容器 focus/blur 事件 + classList .form-group:has(input:focus)
根据内容调整布局 MutationObserver + 条件判断 .card:has(img)
选中状态批量操作 change 事件 + DOM 查询 .table:has(input:checked)
空状态显示判断 childNodes.length 检查 .list:not(:has(.empty-state))

十一、总结

:has() 选择器是 CSS 历史上最重要的特性之一,它填补了"无父选择器"这一长期空白。通过本文的学习,你应该掌握:

  • :has() 的核心语法和浏览器支持情况
  • 与 :not()、容器查询的组合使用
  • 表单验证、卡片布局、导航菜单、数据表格等实战场景
  • 性能优化策略与渐进增强方案

关键要点:

  • :has() 不是真正的"父选择器",而是"关系选择器"
  • 使用前务必检查目标浏览器的支持情况
  • 避免使用 * 通配符和过度嵌套
  • 配合 @supports 实现渐进增强
  • 优先使用 class 选择器而非属性或标签选择器

现在 :has() 的覆盖率已经超过 95%,是时候在你的项目中拥抱这个强大的选择了!

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部