用CSS :has() 写了几个刁钻组件,我现在有点同情JavaScript了

2026-08-25 0 607

上个月接了一个后台项目,里面有这么个需求:两个输入框,第二个输入框的边框颜色要跟着第一个输入框的内容变化——第一个有值时边框变蓝,没有值时就恢复灰色。以前这种逻辑我一般用 `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,新手容易把自己绕进去。

用CSS :has() 写了几个刁钻组件,我现在有点同情JavaScript了
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 css 用CSS :has() 写了几个刁钻组件,我现在有点同情JavaScript了 https://www.taomawang.com/web/css/2638.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务