CSS :has() 实战:从父选择器到表单状态管理,这几个案例拿来即用

2026-09-01 0 518

前阵子在做一个内部运营后台,要求侧边栏的菜单项如果有子菜单,就必须在右边显示一个箭头图标;没有子菜单就不显示。这要是以前,我肯定得在后端渲染时多传一个字段,或者在JS里遍历DOM去判断。但这次我直接用了CSS的:has()选择器,一行CSS搞定,连HTML结构都没动。说实话,:has()刚出的时候我以为只是个花架子,真用上了才发现这玩意是能实打实省代码的。

这篇文章就用几个实际案例,带你把:has()最常见的用法过一遍。所有代码都能直接在浏览器里跑,不需要构建工具,也不依赖框架。

先理解 :has() 是什么

:has()被称为“父级选择器”,因为它允许你根据一个元素的后代节点来选中这个元素本身。比如ul:has(li.active),意思就是“选中那些内部包含.active的li的ul”。以前这是不可能做到的,因为CSS只能从父选子,不能从子选父。现在有了:has(),很多以前必须上JS的场景都可以用CSS搞定。

语法并不复杂:

选择器:has(其他选择器) {
  /* 样式 */
}

注意,括号里面的选择器可以是任何复合选择器,包括类、ID、属性、伪类,甚至还能嵌套:has()。功能很强大,但是别用过火。

案例1:侧边栏菜单箭头显示/隐藏

这个就是我开头说的那个需求。菜单结构大致是这样:

<ul class="menu">
  <li class="menu-item">
    <a href="#" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" >仪表盘</a>
  </li>
  <li class="menu-item has-submenu">
    <a href="#" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" >用户管理</a>
    <ul class="submenu">
      <li><a href="#" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" >用户列表</a></li>
      <li><a href="#" rel="external nofollow"  rel="external nofollow"  rel="external nofollow"  rel="external nofollow" >新增用户</a></li>
    </ul>
  </li>
</ul>

原本这两个li长得一样,没有箭头。要求是有子菜单的li右边加个小箭头。用:has()就是一句话:

.menu-item:has(.submenu) {
  background-image: url(arrow.svg);
  background-position: right 10px center;
  background-repeat: no-repeat;
  padding-right: 30px;
}

这个选择器匹配那些内部包含.submenu.menu-item。没有子菜单的li不受影响。也就是说,你只要在HTML里写好了子菜单结构,样式自己会适应,完全不用额外加类。

如果子菜单有多个层级,你甚至可以写成这样:

.menu-item:has(.menu-item) {
  font-weight: bold;
}

意思是:只要li里面还嵌套着li,就给它加粗。这种祖先判断能力,在树形组件里特别管用。

案例2:表单必填星号自动显示

做表单时,经常需要给必填项的label加个红星。如果你的HTML结构是:

<div class="form-group">
  <label for="username">用户名</label>
  <input type="text" id="username" required>
</div>

你可以在label后面放一个星号span,但多少有点占位置。用:has()可以让label自己根据内部有没有required的input来决定是否显示星号,比如用伪元素:

.form-group:has(input[required]) label::after {
  content: "*";
  color: red;
  margin-left: 2px;
}

这样只要是包含必填输入框的表单组,label后面自动带出一个红星。如果你不想用伪元素,也可以调整label的样式,比如加粗或者变色。

再进一步,还可以根据输入框的验证状态来改变背景色。比如:

.form-group:has(input:placeholder-shown) {
  background-color: #fffbe6;
}

这个选择器的意思是:如果某个表单组里的input当前显示的是placeholder(也就是没有输入内容),就把整个表单组的背景变成淡黄色。输入内容后,背景自动恢复,这在制作多步表单时很有用。

案例3:纯CSS卡片悬停效果

以前做卡片悬停时,我们想要鼠标悬停在卡片上,然后让卡片内的某个图标或文字变色,都得写两组CSS:一组hover卡片,一组hover卡片的子元素。现在可以直接用:has()去影响卡片本身,然后再通过后代选择器让内部元素联动。

比如一张商品卡片,希望鼠标悬停时整个卡片加上边框,并且价格文字变红:

<div class="card">
  <img src="product.jpg" alt="">
  <h3&gt无线耳机</h3>
  <p class="price">¥399</p>
</div>

CSS可以这么写:

.card {
  border: 1px solid transparent;
  transition: border-color 0.3s;
}

.card:hover {
  border-color: #ff5500;
}

.card:hover .price {
  color: #ff5500;
}

这个不用:has()也能实现。但如果你想在鼠标悬停到卡片内部某个按钮时,让卡片边框一起变化,那通常只能给按钮外层的父级加事件。现在有了:has(),可以这么做:

.card:has(.buy-btn:hover) {
  border-color: #ff5500;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

意思是:当卡片内的.buy-btn被悬停时,卡片本身也呈现出边框和阴影。这种联动效果用以前的手段几乎必须上JS,现在CSS就完事了。

案例4:导航栏 sticky 状态切换不用JS

有一个很常见的交互:页面顶部导航栏在初始状态是半透明的,一旦向下滚动到内容区某个标题后,导航栏背景变成不透明并加上阴影。传统做法要监听scroll事件,计算scrollTop。其实借助:has()也能做一个简单版的。

前提是你的页面结构里有一个可以被观测到的元素,我们假设它是#page-content。用法是利用:has():target或者IntersectionObserver?可惜:has()不能直接监听滚动位置。但是我们可以用“顶部的导航栏”来观察自己是否“遮住”了某个元素,这需要一些额外的CSS技巧。实际场景中,有一种做法是结合Sticky定位和:has()来增强某个状态。比如很多网站会固定一个顶部进度条,显示阅读进度。用:has()可以做到:当正文区域变得比视口矮时,隐藏侧边栏。

不过这个案例用得有点绕,我觉得不如前面几个直观。这里就不展开讲了。我更想说的是,:has()在“状态切换”上的意义不是直接监听scroll,而是让组件根据自身的内部内容或内部的选中状态来决定外部样式。用它来做tab切换很顺手。

案例5:纯CSS Tab切换

Tab切换通常需要JS去切换class。现在用:has()配合radio按钮也能实现,而且不需要写一堆事件。

结构大概是这样:

<div class="tabs">
  <input type="radio" name="tab" id="tab1" checked>
  <label for="tab1">热门</label>

  <input type="radio" name="tab" id="tab2">
  <label for="tab2">最新</label>

  <div class="panel panel1">热门内容</div>
  <div class="panel panel2">最新内容</div>
</div>

然后CSS用:has()根据选中的radio来切换面板的显示:

.tabs:has(#tab1:checked) .panel1 {
  display: block;
}
.tabs:has(#tab2:checked) .panel2 {
  display: block;
}
.panel {
  display: none;
}
/* label高亮样式 */
.tabs:has(#tab1:checked) label[for="tab1"],
.tabs:has(#tab2:checked) label[for="tab2"] {
  background: #eee;
}

这里#tab1:checked就是“当radio被选中时”,然后外层.tabs因为包含这个被选中的radio,就会触发面板显示。效果跟JS写的一样,而且连键盘操作和状态保持都是浏览器原生支持。

不过需要注意,这种写法需要隐藏掉radio本身,不然页面上会有小圆点。你肯定想知道怎么隐藏,用position:absolute; opacity:0即可,但这不是本文重点。

这个案例的扩展空间很大

你可以把radio换成checkbox,做成折叠菜单;也可以组合多个:has()实现更复杂的联动。比如一个表单里,只有勾选了“其他”选项,才出现一个文本输入框:

<div class="wrapper">
  <label>
    <input type="checkbox" id="other">
    <span>其他</span>
  </label>
  <input type="text" class="other-input" placeholder="请输入">
</div>
.wrapper:has(#other:checked) .other-input {
  display: block;
}
.other-input {
  display: none;
}

这种纯CSS的条件交互完全可以替代简单JS中的show/hide操作。

实用功能和注意点

有一些小细节,如果你刚上手:has(),很容易踩坑。我把它写出来避雷:

  • 别用来做全页面级别的全局选择。比如body:has(...),因为浏览器在计算页面样式时,使用:has()可能会影响性能,尤其是页面很大很复杂的时候。尽量缩小使用范围,加一个容器类限定。
  • 不要让:has()嵌套太深。比如div:has(div:has(span))这种虽然能跑,但性能堪忧。能拆开就拆开。
  • 它和样式权重的关系。伪类本身的权重跟普通的类选择器一样,所以如果你想让:has()覆盖其他样式,可能需要写得更具体。举个例子:.a:has(.b) .c的权重等于.a .c加上`.b`的权重,反正具体可查,别搞混。
  • 不要用它替代所有JS逻辑。比如拖拽排序、复杂交互,还得老老实实写脚本。但状态切换这种活,用:has()很合适。

写在最后

我自己的感受是,:has()真正强大的地方不在于“选父元素”,而在于它把“元素内部的状态”变成了一种可以驱动外部样式的机制。以前你用JS去根据状态加class,但是现在你用CSS就能根据状态自动调整。这减轻了前端的逻辑负担,也让代码更不易出错。

尤其是配合表单和交互组件时,以前得写十几个事件监听器的地方,现在一个:has()就完成了。如果你还没在项目里用过它,建议找个简单的小组件试试手,相信我你会回来把它用满的。

CSS :has() 实战:从父选择器到表单状态管理,这几个案例拿来即用
收藏 (0) 打赏

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

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

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

淘吗网 css CSS :has() 实战:从父选择器到表单状态管理,这几个案例拿来即用 https://www.taomawang.com/web/css/2683.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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