CSS :has() 选择器实战笔记:终于可以选中父元素了

2026-08-08 0 787

入行第五年,写CSS的时候总会遇到一些“要是能选中父元素就好了”的瞬间。以前只能加class或者用JS曲线救国。直到今天,我发现 :has() 这个新选择器已经能在不少产线项目里跑了,于是决定把最近用 :has() 写的一些真实案例整理出来,给没怎么上手的朋友当个参考。

它之所以让我兴奋,是因为它终于让CSS有能力“根据子元素的某种状态来决定父元素的样式”,甚至可以根据后面的兄弟元素搞定前面元素的样式。我直接说结论:只要你能接受它,很多老方案都得改。

先看看 :has() 到底长什么样

语法很直观,从你的选择器后面加上 :has(),括号里写你要找的后代或者兄弟:

/* 选中含有动态类 .active 的 li */
li:has(> .active) {
  background: #f0f0f0;
}

/* 选中直接包含 button 的 section */
section:has(> button) {
  padding: 1rem;
}

:has() 的作用就是“包容”。如果括号里的选择器能匹配到当前元素的某个后代,那当前元素就被选中。不是选子元素,是选“祖先”本身,所以很多人管它叫父选择器。

注意它不只是能选后代,还可以配合 + 选中前面的兄弟:

/* 被同级 h2 紧挨着的 p 会被选中 */
p:has(+ h2) {
  font-weight: 600;
}

换句话说,我可以根据“后面有没有一个元素”来给前面的元素加样式,这个以前完全做不到。

第一个案例:表单验证状态不污染JS

最让我觉得清爽的是一个表单场景。以前我们校验必填项失败时,都是给input加一个.error类,再写 .error { border-color: red; }。但表单结构往往是:

<div class="field">
  <label for="username">用户名</label>
  <input id="username" type="text" required>
  <span class="error-msg">请输入用户名</span>
</div>

出错时我们希望整个.field边框变红,错误提示显示出来。以前得用JS同时操作两个兄弟元素,现在完全可以让CSS自己判断:

.field {
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 8px;
  transition: border-color 0.2s;
}

/* 当input是invalid并且是页面里被操作过的 */
.field:has(input:user-invalid) {
  border-color: #d93025;
}

.field:has(input:user-invalid) .error-msg {
  display: block;
}

.error-msg {
  display: none;
  color: #d93025;
  font-size: 13px;
}

:user-invalid 是CSS新出的伪类,表示用户修改后仍然不符合校验规则的输入框。配合 :has(),就能做到HTML零改动,JS零介入,纯CSS完成错误状态展示。

这个案例里最重要的一点是,:has() 可以根据“input的状态”来影响整个.field容器,包括其他子元素。原来我们总会在JS里为这块逻辑写各种addEventListener,现在CSS都包了。

第二个案例:导航菜单自动高亮,不用JS判断URL

以前做侧边导航的时候,为了让当前页面对应的菜单高亮,往往要后端输出一个current类,或者前端在路由切换的时候手动去改类名。但如果你用的是静态页面或者样式隔离很严格,手写类名很烦。

现在我直接利用当前页面body上的页面标识,比如body.page-index:

<nav class="menu">
  <a href="/dashboard" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  data-page="dashboard">仪表盘</a>
  <a href="/user" rel="external nofollow"  rel="external nofollow"  data-page="user">用户管理</a>
  <a href="/setting" rel="external nofollow"  data-page="setting">设置</a>
</nav>

然后CSS这么写:

body.page-dashboard .menu a[href="/dashboard" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ] {
  background: #1a73e8;
  color: white;
}

body.page-user .menu a[href="/user" rel="external nofollow"  rel="external nofollow" ] {
  background: #1a73e8;
  color: white;
}

/* 不用一个个写了,直接用 :has() 根据body的类自动匹配 */
.menu:has(a[href="/dashboard" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ]) {
  /* 这里没法知道是哪个body类,没意义 */
}

那 :has() 在这里怎么用?其实可以反过来写,让链接自己根据body类变化:

.menu a {
  color: #333;
}

body:has(> .menu a[href="/dashboard" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ]) .menu a[href="/dashboard" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" ] {
  background: #1a73e8;
  color: #fff;
}

这看起来很绕,但你可以理解为“如果 body 下面存在一个指向/dashboard的链接,就把这个链接变亮”。只要body的类变了,:has() 的条件就会自动重新匹配。你在代码里只需要维护body page-xxx这个类,菜单高亮完全由CSS控制。

以后增加新页面,不用再写新的CSS规则,只需要保持a链接的href和body类名一致。我上次把它用在电子文档的侧边导航里,删掉了整整一个函数。

第三个案例:卡片里有视频或者图片时,调整内边距和标题大小

产品列表里可能有纯文字的卡片,也可能有带封面的卡片。以前这种差异必须通过不同类名来区分。现在 :has() 可以根据卡片里有没有图片/视频来修改卡片自身的样式。

.Card {
  padding: 24px;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
}

/* 卡片里有 img 元素时缩小内边距,让图片更加舒展 */
.Card:has(img) {
  padding: 0;
  overflow: hidden;
}

/* 卡片里有 video 时标题文字小一号 */
.Card:has(video) .Card-title {
  font-size: 1.1rem;
}

/* 卡片里同时有图片和标题时,把标题叠加在图片上 */
.Card:has(img) .Card-body {
  padding: 16px;
  margin-top: -48px;
  position: relative;
  background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);
  color: white;
}

这种写法,让模板只用维护一个Card结构。系统输出时,有图自然显示图,没图就显示普通文字模式,CSS自动适配,样式代码量还少了不少。

我在博客的文章列表卡片里用了这个思路。缩略图存在与不存在,不再需要模板判断,直接交给CSS,模板代码更简洁。

第四个案例:防止页面滚动条跳动

打开一个很长文档时,右边有没有滚动条会影响页面内容宽度。以前我们用JS添加滚动条占用宽度的补丁,但也可以用 :has() 来针对顶级元素做处理:

html:has(scrollbar) {
  scrollbar-gutter: stable;
}

这个选择器匹配的是html元素,条件是只要html里存在scrollbar(滚动条),就启用scrollbar-gutter。注意这个scrollbar是一个伪元素,:has() 其实可以匹配伪元素吗?严格来说不能匹配伪元素,所以这个写法其实不准确。我平时更常用的是:

html {
  scrollbar-gutter: stable;
}

不建议用 :has() 去检查伪元素,因为它的选择器语法只能选真实DOM元素。别踩坑。

性能真的那么差吗?谈谈我的体验

网上很多人担心 :has() 的性能。这个函数看起来会让浏览器做很多额外遍历,但现代浏览器对 :has() 做了很多优化,而不是你写的 :has() 有多少复杂,浏览器就会暴力扫描全部DOM。我的经验是:避免在 :has() 的括号里写特别复杂的选择器,尤其不要嵌套过多的 :has(),日常足够快。

比如下面这种就有点危险:

article:has(section:has(p:has(span))) { ... }

我自己尽量控制最多一层,括号里选择器保持简单,就这样跑得没什么问题。如果你是在非常庞大的列表页面里用,建议做一次性能测试。

与JavaScript配合的边界

:has() 也可以配合某些行为,比如当input被focus时让label变色:

.field:has(input:focus) label {
  color: #1a73e8;
}

这就省去了JS的focus和blur监听。实际上你能用:has() 编写出很多年前根本不敢想的纯CSS交互。

再说一下兼容性。Chrome/Edge/Safari/Firefox 从去年开始都陆续支持了,现在新版本浏览器基本没问题。唯一需要注意的就是还在维护老项目时,最好看看用户群里有没有老浏览器。

如果想稳一点,可以把 :has() 写在增强样式里,用 @supports 包一层:

@supports selector(:has(a)) {
  .item:has(> .active) {
    border-color: blue;
  }
}

这样不支持的浏览器只是没有特殊效果,不会出现样式崩坏。

一个日常经常用到的“列表项数量”技巧

:has() 还能结合 :nth-child() 根据总数来设置样式。例如列表只有一项时,我想让那个项显示不同的圆角;超过四项时,最后一项不加下边线。以前必须动态加类,现在:

li:only-child {
  border-radius: 12px;
}

li:last-child:has(~ li) {
  border-bottom: none;
}

第一行表示如果li是唯一的子元素,就加大圆角。第二行表示如果li后面还有别的兄弟li,那就不添加下边线。这里用了兄弟选择器 ~ 放在 :has() 里,意思是“存在一个满足条件的兄弟元素”。这种写法很简洁,而且不影响可读性。

在过去这种逻辑一直活在JavaScript里。要把这样的状态同步到CSS,通常要写一段matchMedia或者MutationObserver。如今一行CSS,清爽到不行。

总结感言

:has() 给我的感觉就是“CSS终于有判断能力了”。它没有改变CSS布局方式,但改变了我编写组件的思路。以前写样式总是想怎么给每一个状态一个类名,现在我会优先想“这个组件在什么子结构下应该变成什么样”。

你在实际开发中别怕用它。从简单的表单状态、导航高亮、卡片布局开始,慢慢你会发现很多JavaScript都可以从里面挪走。CSS正在变得越来越强大,倒不是说要完全抛弃JS,但至少对于一些轻量交互,CSS自己就能搞定,体验也更顺滑。

希望这篇文章能帮你打开新世界的大门。如果有更多好玩的实践,欢迎在评论区交流。

CSS :has() 选择器实战笔记:终于可以选中父元素了
收藏 (0) 打赏

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

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

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

淘吗网 css CSS :has() 选择器实战笔记:终于可以选中父元素了 https://www.taomawang.com/web/css/2509.html

常见问题

相关文章

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

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