CSS :has() 选择器实战:没有它之前我们写了多少冗余 JS

问题场景

产品提了个"简单"需求:

卡片里如果有图片,卡片标题用大字号并加间距;如果只有文字,保持紧凑。

你第一反应:加个类名嘛。结果后端返回的数据里没有「是否含图」字段,你得在渲染时额外判断,再给卡片加一个 .has-image 类。

更麻烦的来了------表单校验错误高亮 :输入框里有错误提示文字时,输入框边框变红;导航高亮:当前菜单项的子菜单展开时,父级菜单加粗。这些全部靠 JS 手动维护类名。

一行 CSS 就能解决的事,为什么我们要写那么多 JS?


原因分析:CSS 曾经"单向"的局限

传统 CSS 选择器只能从左到右 (父 → 子/后代):.parent .child {}

但我们经常需要从右到左 (子 → 父)的判断:这个容器里有图片吗?这个列表里有错误项吗?

以前只能:

  1. JS 监听 DOM 变化 → 2. 判断子元素状态 → 3. 给父元素加类名 → 4. CSS 根据类名设置样式

这个流程又臭又长,而且在动态数据场景下极易出 bug(比如异步加载后忘记重新判断)。

:has() 把第 1-3 步全干掉了------CSS 自己就能"回头看"。


实操代码 / 要点总结

🎯 场景一:卡片内含图片时自动扩容

css 复制代码
/* 传统方案 --- 每张卡片都要在 JS 里手动加类名 */
.card.has-image { padding: 24px; }
.card.has-image .title { font-size: 1.5rem; }

/* :has() 方案 --- 纯 CSS,零 JS */
.card:has(img) {
  padding: 24px;
}
.card:has(img) .title {
  font-size: 1.5rem;
  margin-bottom: 12px;
}

:has(img) 直译:"这个 .card 元素,如果它内部有 img 的话......"

注意:has() 检查的是后代 ,不只是直接子元素。.card:has(> img) 可以缩小为只查直接子代。

🚨 场景二:表单校验状态 + 错误提示高亮

以往:后端返回校验结果 → 循环塞错误文案 → 给 .form-group.has-error

现在:

css 复制代码
/* 输入框组内有 .error-msg 时,输入框变红 */
.form-group:has(.error-msg) input {
  border-color: #e53e3e;
  box-shadow: 0 0 0 2px rgba(229, 62, 62, 0.2);
}

/* 校验通过时显示绿勾(无错误 + 已 touched) */
.form-group:has(input[required]:valid):not(:has(.error-msg))::after {
  content: "✓";
  color: #38a169;
}

整个表单校验的视觉反馈:零 JS。错误文案一插入 DOM,样式自动生效。

🧭 场景三:导航菜单 --- 子菜单展开高亮父级

css 复制代码
/* 侧边栏 --- 如果有子菜单展开,父级高亮 */
.nav-item:has(.sub-menu.is-open) > .nav-link {
  font-weight: 700;
  background: rgba(59, 130, 246, 0.08);
}

/* 三级联动:父级展开 → 祖父级也高亮 */
.nav-group:has(.sub-menu.is-open) > .group-title {
  color: #2563eb;
}

不需要 JS 去冒泡、匹配、切换类名。:has() 天然支持嵌套回溯。

🧩 场景四:列表空状态 --- 一行 CSS 代替 v-if

css 复制代码
/* 列表为空时显示提示 */
.list-wrapper:has(> .list-item) .empty-state {
  display: none;
}

/* 反向:只有空的时候才显示 */
.list-wrapper:not(:has(> .list-item)) .empty-state {
  display: block;
}

这意味着你不需要 在 Vue 里写 v-if="list.length" 来切换空状态------CSS 自己判断 DOM 里有没有元素。

⚡ 场景五:栅格布局自适应

css 复制代码
/* 如果某个 grid-item 包含长文本,让它跨两列 */
.grid:has(.long-text) .grid-item.long-text {
  grid-column: span 2;
}

性能注意(踩坑预警)

:has() 目前不是万能药,有几个性能陷阱:

  1. 不要给全页面用body:has(*) 这种选择器会让浏览器重新计算几乎所有元素,极其昂贵
  2. 限制范围 :尽量配合类名/ID 缩小范围,.card:has(img) 优于 div:has(img)
  3. 不要嵌套太深.a:has(.b:has(.c)) 这种三层 :has() 会让浏览器"三思而行"
  4. :not() 组合要谨慎:not(:has(...)) 开销更大,能用 :has() 正向匹配就别取反

实战经验 :在列表/卡片/表单等局部容器 里用 :has(),性能完全没问题。不要用在全局重置或根级选择器上。


要点总结

  1. :has() 是 CSS 的"父选择器",能根据子元素状态反向影响父级样式------过去只能靠 JS 搞
  2. 表单组件是最佳应用场景:错误状态、校验反馈、空态展示,全 CSS 搞定
  3. 导航菜单 的父子联动高亮,:has() 天然比 JS 冒泡更优雅
  4. 性能敏感:限制在局部容器内使用,避免全局或深层嵌套
  5. 浏览器兼容性 :截至 2026 年,:has() 已获所有现代浏览器(Chrome 105+, Safari 15.4+, Firefox 121+)支持,可以放心在生产环境使用

记住这条原则:能用 CSS 解决的问题,就不要用 JS ------:has() 把这句话又往前推了一大步。

相关推荐
梨子同志1 小时前
TypeScript
前端
星栈1 小时前
LiveView 表单真香,但 changeset 也真会坑人:实时校验、错误展示、前后端校验合一
前端·前端框架·elixir
Slice_cy1 小时前
JavaScript(ES6)
前端
用户298698530141 小时前
在 React 中使用 JavaScript 合并 Excel 文件
前端·javascript·react.js
橘子星1 小时前
JavaScript this 指向全解实战指南
前端·javascript
何出无名之师1 小时前
AIDL的一次调用链路追踪之二,如何和驱动打交道
前端
weedsfly1 小时前
JS垃圾回收:从原理到项目实战,彻底根治内存泄漏
前端·javascript·面试
Jcc1 小时前
虚拟 DOM 是什么?从 Snabbdom 理解 Vue 的 DOM 更新机制
前端