CSS :has() 选择器实战:五招让前端代码少一半JS

2026-08-23 0 176

上星期在做一个后台管理系统,遇到个需求:表单里的输入框如果校验不通过,要红边框加红色提示文字。刚开始我打算用 JS 去监听 input 事件,然后 toggle class。后来一想,不对,这不就是 CSS 能干的活吗?直接上 :has() 选择器,一行代码搞定,还不用写 JS 逻辑。

今天我就把这几个我在项目里真实用过的 :has() 场景写出来,每一个都有完整代码,直接复制就能用。

先搞清楚 :has() 是个啥

其实它就是个选择器,作用是根据元素内部的状态来选它自己。举个例子,div:has(span) 就是选中里面包含 span 的 div。咋一听很拗口,但用熟了你就会觉得它就是 CSS 版的“包括”或“存在”。

浏览器支持已经不需要担心了,Chrome 105、Safari 15.4、Firefox 121 之后全绿,用户基本都覆盖到。

案例1:表单字段校验状态样式

我之前写了一个注册表单,里面有邮箱和手机号两个必填项。需要实现:输入合法时输入框边框变绿,非法时变红,标签文字也要变色,并且不用 JS 去盯着。

<form class="signup-form">
  <div class="field">
    <label for="email">邮箱</label>
    <input type="email" id="email" required>
  </div>
  <div class="field">
    <label for="phone">手机号</label>
    <input type="tel" id="phone" required pattern="[0-9]{11}">
  </div>
</form>

CSS 这么写就完事了:

.field:has(input:invalid) label {
  color: #e44d4d;
}
.field:has(input:invalid) input {
  border-color: #e44d4d;
  background: #fff8f8;
}

.field:has(input:valid) label {
  color: #2e9e5b;
}
.field:has(input:valid) input {
  border-color: #2e9e5b;
}

看,当输入框是 :invalid 状态时,父级 .field 的 label 和 input 都会变红。浏览器自带的 HTML5 校验完全够用,根本不需要写 JS 去判断正则。我后来把这个套路用在所有表单页面上,省了不少事。

案例2:卡片悬停时让图片和文字同时响应

产品经理要求做一个卡片列表,鼠标悬停时卡片本身要微微上浮,同时卡片里的图片要放大一点。以前你要给卡片加 mouseenter 和 mouseleave 事件,然后操作 CSS class,现在写好就好。

<div class="card">
  <div class="card-img">
    <img src="photo.jpg" alt="示例图">
  </div>
  <div class="card-body">
    <h3>卡片标题</h3>
    <p>一段描述文本</p>
  </div>
</div>
.card {
  transition: transform 0.3s, box-shadow 0.3s;
}

.card:has(.card-img:hover) {
  transform: translateY(-6px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
}

.card:has(.card-img:hover) .card-img img {
  transform: scale(1.05);
}

.card-img {
  overflow: hidden;
}

.card-img img {
  transition: transform 0.3s;
  display: block;
  width: 100%;
}

这里用到了 :has(.card-img:hover) 去判断鼠标是否悬停在图片区域。一开始我以为这么写会不会有性能问题,实测下来完全没感觉。而且这个写法特别容易维护,后续加其他交互只需要在 :has() 里加条件就好。

案例3:列表项数量不同,布局自动变化

后台有一个数据看板,要展示最近的热搜标签。有时候只有两三个,有时候十来个。我希望标签数量少的时候单行显示,数量多了就自动换行。这个用 :has() 配合 :nth-child 就能做到。

<ul class="tag-list">
  <li>热门</li>
  <li>爆款</li>
  <li>特惠</li>
  <li>新品</li>
  <li>推荐</li>
</ul>
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* 当列表里的 li 在第四个及以上时,换行并限制最大宽度 */
.tag-list:has(li:nth-child(4)) {
  max-width: 320px;
}

/* 当 li 数量超过 6 个时,变成两列网格 */
.tag-list:has(li:nth-child(6)) {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  max-width: 460px;
}

这里有个小技巧::has(li:nth-child(4)) 意思是“存在至少4个 li”。如果你想知道是不是正好4个,可以结合 :not(:has(li:nth-child(5))) 来写,不过实际业务中“大于等于”比“等于”更常用。

案例4:模态框打开时锁住背景滚动

做弹窗的时候总有一个需求:弹窗出现后,后面的页面不能滚动。以前我们经常在 JS 里给 body 加 overflow: hidden,现在用 :has() 也能管。前提是模态框和 body 在同一个 DOM 树里,且模态框状态由 class 控制。

<body>
  <div class="page-content">
    这里是背景内容
  </div>

  <div class="modal-overlay hidden">
    <div class="modal-box">
      弹窗内容
    </div>
  </div>
</body>

当我去掉 .hidden 类时,body 就应该锁定滚动:

body:has(.modal-overlay:not(.hidden)) {
  overflow: hidden;
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-overlay.hidden {
  display: none;
}

你只需要在 open 时移除 hidden,在 close 时添加回去,剩下的 CSS 自动处理滚动锁定,不需要写多余的 JS 去改 body 样式。

不过要留意一个坑:如果 body 里面没有直接包含模态框,而是通过组件渲染,:has() 是跨组件选不了的。这种场景还是老老实实 JS 吧。

案例5:导航菜单当前项的高亮指示

导航栏经常用 ul 和 li 来实现。以前要高亮当前菜单,往往在生成标签时判断一下当前路径,然后给当前 li 加个 active 类。其实可以用 :has() 根据 a 的 href 或者特定状态来高亮父级 li。这里我就用 aria-current 当一个例子。

<nav>
  <ul>
    <li><a href="/" rel="external nofollow" >首页</a></li>
    <li><a href="/docs" rel="external nofollow"  aria-current="page">文档</a></li>
    <li><a href="/about" rel="external nofollow" >关于</a></li>
  </ul>
</nav>
nav li:has(a[aria-current="page"]) {
  background: #f0f0f0;
  border-radius: 4px;
}

nav li:has(a[aria-current="page"]) a {
  color: #07c;
  font-weight: 600;
}

这样服务端渲染时只需要输出正确的 aria-current,样式就自动匹配,无需额外写 class。

再说点避坑经验

:has() 看起来美好,但用的时候有几个地方要小心。

优先级不一样。 ul:has(li) 的优先级等于 ul li 加上一个伪类。所以如果你在另外地方写了更具体的样式,可能会覆盖掉 :has() 的效果。复杂页面里最好用更明确的选择器,比如 .tag-list:has(li:nth-child(6))

不能嵌套太多层。 比如 div:has(span:has(b)),这在 Chrome 上没问题,但你得考虑维护成本。我自己写代码的时候如果超过一层 :has,就会反思是不是该换别的写法。

性能问题不用太担心,但别在动画里用。 如果你做一个持续过渡的动画,同时用 :has() 去判断某个内部状态,可能会引起额外的重算。实测下来平时点击、悬停都没事,但别把这东西用在 requestAnimationFrame 驱动的动画上。

总结

说实话,掌握 :has() 之后,我写交互样式的速度提升了不少。以前要写事件监听、改 DOM class,现在CSS里就能把这个逻辑表述清楚。而且代码可读性还更好,看到 :has(input:invalid) 你就知道这一段是关于输入校验的样式。

希望这几个案例能给你启发,不用全盘照搬,挑一两个用到你的项目里试试,应该会回来感谢我的。

CSS :has() 选择器实战:五招让前端代码少一半JS
收藏 (0) 打赏

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

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

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

淘吗网 css CSS :has() 选择器实战:五招让前端代码少一半JS https://www.taomawang.com/web/css/2595.html

常见问题

相关文章

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

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