上个月接了一个后台项目,里面有这么个需求:两个输入框,第二个输入框的边框颜色要跟着第一个输入框的内容变化——第一个有值时边框变蓝,没有值时就恢复灰色。以前这种逻辑我一般用 `input` 事件监听,然后给元素加 class。但那天我不知哪根筋搭对了,手一抖搜了一下 CSS 能不能选中“包含某个子元素的父级”,结果就撞上了 `:has()` 选择器。
测试完以后,我发现自己写了好多年的联动判断,有很大一部分其实用不着 JavaScript。这篇文章就把我这段时间用 `:has()` 解决的真实业务场景拎出来讲讲,每个案例都有完整代码,拿过去就能改。
先说一句,:has() 到底是个啥
它就是一个选择器,用来“检查某个元素里有没有符合指定条件的子元素/后元素”,有就选中这个父元素。比如 div:has(> img) 就是选中“直接子元素里有 img 的 div”。
这玩意儿最厉害的地方不是能选儿子,而是它让 CSS 第一次有了“根据子元素状态来给祖先元素换样式”的能力,像 :focus-within 的加强版。以前这种关系必须用 JS 监听,现在 CSS 自己就能干。
案例一:我删掉了别人写的 validate JS
那个后台项目里有几个表单页,原本用了一段很长的 JS 校验:输入非空后给父容器加一个绿边,为空时候加红边。我观察了下,大部分校验逻辑其实只发生在“用户填过内容”之后,用 `:has()` 加 CSS 完全可以做到。
假设现在有一个登录表单:
<form class="login-box">
<div class="field">
<input type="text" placeholder="用户名" required>
<span class="tip">用户名必填</span>
</div>
<div class="field">
<input type="password" placeholder="密码" required>
<span class="tip">密码必填</span>
</div>
</form>
原来同事写的 JS 大概长这样:监听每个 input 的 blur,然后判断 value 长度,再给 `parentElement` 加上 `.error` 或者 `.success` 类。
现在用 `:has()` 配合表单原生的 `:valid`、`:invalid`,效果一样,甚至还能少管一次事件。
/* 只要输入框内容非空并且合法,字段组边框就是绿色 */
.field:has(input:valid) {
border: 1px solid #2ecc71;
}
/* 用户已经输入了内容,但内容不合法(比如密码太短)就变红
:not(:placeholder-shown) 是为了避免空输入框误报 */
.field:has(input:invalid:not(:placeholder-shown)) {
border: 1px solid #e74c3c;
}
/* 提示文字默认半透明,当对应输入框有内容时才亮起来 */
.field:has(input:placeholder-shown) .tip {
opacity: 0.3;
}
.field:has(input:not(:placeholder-shown)) .tip {
opacity: 1;
}
注意这里我用的是 `.field`,就是父容器。`:has(input:valid)` 会在任何一个子 input 合法时选中父级,所以两个字段会自己影响自己的边框。
我删掉了原先几十行 validate.js,页面功能一点没少。浏览器原生的表单校验样式虽然丑了点,但可以用 `::placeholder` 之类的伪元素继续美化,不影响业务逻辑。
案例二:卡片列表根据“有没有图”自动切换布局
产品列表页之前在模板里写了两种卡片:一种带图,一种纯文字。后来产品经理说能不能同一个小卡片,有图片的时候变成左图右文,没图片的时候只显示上下排列的文字。以前这种需求要么后端给一个字段控制 class,要么前端用 v-if 在模板里搞两套样式。
现在我用 `:has(img)` 一行规则就干完了。
<ul class="card-list">
<li class="card">
<img src="thumbnail.jpg" alt="">
<div class="card-body">
<h3>商品标题</h3>
<p>商品描述文字,可能比较长。</p>
<button>去看看</button>
</div>
</li>
<li class="card">
<div class="card-body">
<h3>纯文本商品</h3>
<p>这种卡片没有图片,需要布局别太挤。</p>
<button>去看看</button>
</div>
</li>
</ul>
对应的 CSS 可以这么写:
/* 默认卡片是垂直堆叠 */
.card {
display: flex;
flex-direction: column;
padding: 16px;
border: 1px solid #eee;
border-radius: 12px;
}
/* 一旦卡片里有图,就变为水平布局 */
.card:has(img) {
flex-direction: row;
gap: 16px;
}
/* 有图的时候让文字区块右边撑满 */
.card:has(img) .card-body {
flex: 1;
}
这种“有图片”的判断以前必须用 JS 先遍历一遍给卡片加类,或者等图片加载完成再判断。现在不用了,CSS 自己能根据 DOM 里的实际情况反馈到布局上。
而且当我用 JS 动态给某个卡片插入一张图片时,布局会立刻发生改变,不需要重新渲染。
案例三:单选按钮选中“其他”,就显示附加输入框
设置页经常有这种操作:三个单选按钮,最后一个叫“其他”,选中“其他”后旁边会出现一个文本框让用户输入具体内容。经典做法是监听 radio 的 change 事件,然后切换一个实现准备好的隐藏区块。
用 `:has()` 我们可以把这个“联动”也写进 CSS。
<div class="source-wrap">
<label>
<input type="radio" name="source" value="online">线上
</label>
<label>
<input type="radio" name="source" value="offline">线下
</label>
<label>
<input type="radio" name="source" value="other">其他
</label>
<div class="extra-field">
<textarea placeholder="请补充渠道名称"></textarea>
</div>
</div>
只要选中“其他”,就让它出现:
/* 默认把附加输入框藏起来 */
.extra-field {
display: none;
margin-top: 12px;
}
/* 当容器里有被选中的“其他”单选框时,显示 .extra-field */
.source-wrap:has(input[value="other"]:checked) .extra-field {
display: block;
}
效果和 JS 一样,但代码量少一半,也不容易踩“removeEventListener 忘掉导致内存泄漏”的坑。
还有很多刁钻用法,比如控制 body 滚动
有时候我们想让弹窗打开时背景页面不能滚。用 JS 的话得计算 scrollbar 宽度再给 body 加 overflow: hidden。现在可以直接检查页面上有没有打开的 dialog 或者 popover:
body:has(dialog[open]) {
overflow: hidden;
}
同理,如果你有一个表格组件,想让“包含某种数据的行”整体高亮,可以不用遍历每一行,直接写:
tr:has(.highlight) {
background: var(--highlight-bg);
}
甚至你还能用 `:has()` 修复“input 在 flex 容器里被压缩”的经典问题,只要给它所在容器加一个`:has(input)`,然后重新设置 min-width。
几个必须要说的注意事项
性能不是灾难,但你最好别写太复杂的选择器。比如 `body:has(div:has(span:has(a))):has(.x)` 这种嵌套会让浏览器匹配崩溃。我自己的习惯是尽量只套一层,最多两层。像 `div:has(> img)` 就是很高效的写法。
`:has()` 不能用在伪元素里面。比如 `::before:has()` 没意义,伪元素不能作为普通选择器的基石。
兼容性现在已经很稳了。前几天特意查了下,Chrome/Edge 105+、Safari 15.4+、Firefox 121+ 全部支持。如果你要支持特别老的 WebView,可以加一个 `@supports selector(:has(*))` 做降级。
比如你想优雅降级:
@supports selector(:has(*)) {
.field:has(input:invalid) {
border-color: red;
}
}
/* 不支持 :has() 时走这段冗余样式 */
.field.invalid-js {
border-color: red;
}
只要 JS 仍然负责备份,CSS 负责增强,那这套方案不会出乱子。
我现在的习惯是先写 CSS,再补 JS
经过这几个案例,我慢慢转变了路子:遇到联动样式需求,先想一想纯 CSS 能不能实现。以前觉得不可能的事,现在发现只是以前不敢想。`:has()` 不是银弹,它解决的是“父子状态关联”的问题,而真正的业务逻辑还是得 JS 来。但确实有不少 DOM 样式联动,是从前端脚本里可以删掉的。
至少,那个后台项目的登录页、卡片列表、设置页,我都把 JS 监听删干净了。功能没坏,代码还短了一截,维护起来也省心。
如果你正准备在项目里推广这个玩法,建议从表单校验这种小地方开始。先把一条简单的 `:has(:invalid)` 用上,再慢慢扩大范围。不要一口气把所有逻辑换成 CSS,新手容易把自己绕进去。

