CSS :has() 选择器终于能用了?这几个实战场景让我删掉大量JS

2026-08-20 0 576

上周做前端页面,碰到一个需求:输入框里面如果有内容,就显示旁边的清空按钮;如果是空着就隐藏。以前这种逻辑必然要写JS监听input事件,然后去切一个class。后来看到一个同事在代码里写了一个 :has(),我才想起来——原来这个选择器已经可以随便用了。试了一下,确实香,我一晚上把项目里十几个类似的交互全改了,几乎没有再写一行JS。

先说一句 :has() 是干嘛的

:has() 是一种关系选择器,它能根据某个元素的后代、兄弟或者后续状态去匹配这个元素。比如你有一个 <form>,里面有一个 <input required>,你想判断这个 <form> 是否有一个必填项,那么可以直接写 form:has(input:required)。这听起来像是“父选择器”,但它比父选择器更强大,因为它可以写任意关系。

先看最基本的写法:

/* 选择包含 .icon 元素的 div */
div:has(.icon) { background-color: #f0f0f0; }

读起来就是“一个div,它内部有.icon的,就给它加背景色”。

注意,:has() 没有改变样式优先级,也不影响DOM树,它只是一个匹配条件。

实战场景一:输入框有内容时显示清空按钮

先画一个简单的结构,把按钮放在输入框后面,利用 :has() 控制按钮的显示。

<div class="field">
    <input type="text" placeholder="请输入关键词">
    <button class="clear-btn">×</button>
</div>

以前用JS写是这样的:监听input事件,判断value长度,然后给父元素加一个active类。现在用 :has() 可以这样:

.clear-btn { display: none; }
.field:has(input:placeholder-shown) .clear-btn { display: none; }
.field:has(input:not(:placeholder-shown)) .clear-btn { display: block; }

简单解释一下: :placeholder-shown 这个伪类表示“占位符正在显示”,也就是说输入框为空。所以第一个规则是,输入框为空时,不显示按钮;第二个规则是,如果输入框不是空,也就是有内容时,按钮显示出来。完全不需要JS。

可能有同学问,如果不用placeholder,而是用value属性,怎么判断?可以用 input:not(:placeholder-shown) 已经是标准办法,但有些浏览器对value属性识别有兼容性问题,目前最佳实践就是配合 placeholder 使用。

实战场景二:表单校验样式

一个注册页面,当密码输入框的密码长度不够时,给整个表单项加一个红色边框。以前得用JS去监听blur或者input事件,然后添加class。现在用 :has() 加上 :invalid 伪类就能实现。

<div class="form-item">
    <input type="password" required minlength="6">
</div>
.form-item { border: 1px solid #ddd; padding: 5px; }
.form-item:has(input:invalid) { border-color: red; }

你看,只要这个容器内部有无效输入(比如长度不足),这个容器边框就变红。同理,如果是邮箱格式不对,也是 :invalid。想显示错误提示文字,也可以用类似方式:

.error-msg { display: none; }
.form-item:has(input:invalid) .error-msg { display: block; }

这个组合非常实用,可以省掉一整套表单验证的JS代码。

实战场景三:卡片高亮——当卡片中有选中的选项时

做筛选列表时,卡片内部可能有多个可勾选的按钮,当用户至少勾选了一个时,我们想给卡片加一个高亮边框。利用 :has() 可以这样写:

<div class="card">
    <label><input type="checkbox" class="opt"> 选项A</label>
    <label><input type="checkbox" class="opt"> 选项B</label>
    <label><input type="checkbox" class="opt"> 选项C</label>
</div>
.card { border: 2px solid transparent; }
.card:has(.opt:checked) { border-color: #2196f3; }

不仅边框,你还可以给卡片加阴影、改变背景色等等。甚至你可以判断“不是全部选中”的状态,比如:

.card:has(.opt:not(:checked)) { opacity: 0.8; }

不过这会把全选和未选中混在一起,你可以按需使用。

实战场景四:tab导航中当前选中项

以前设置tab高亮,要么用JS绑定点击事件,要么用 :target 或 ~ 兄弟选择器。但有些tab结构已经固定,我们不好改DOM。用 :has() 可以快速实现:

<nav class="tabs">
    <a href="#tab1" rel="external nofollow"  class="tab-item">首页</a>
    <a href="#tab2" rel="external nofollow"  class="tab-item">产品</a>
    <a href="#tab3" rel="external nofollow"  class="tab-item">关于</a>
</nav>
.tab-item { color: #333; }
.tabs:has(.tab-item:target) .tab-item:target { color: red; font-weight: bold; }

这一行样式就实现了一个基于锚点路由的导航高亮。当点击tab2时,URL变为#tab2,然后 .tab-item:target 匹配到它, :has() 确保父级存在target的子元素,最后改那个元素的样式。

当然,如果你不是用href锚点,而是用radio按钮的 :checked 也可以:

<div class="tab-wrap">
    <input type="radio" name="tabs" id="tab1" checked>
    <input type="radio" name="tabs" id="tab2">
    <div class="tab-content">内容一</div>
    <div class="tab-content">内容二</div>
</div>
.tab-content { display: none; }
.tab-wrap:has(#tab1:checked) .tab-content:nth-child(3) { display: block; }
.tab-wrap:has(#tab2:checked) .tab-content:nth-child(4) { display: block; }

不过这里 :has() 的作用不是特别明显,但思路是共通的。

实战场景五:基于容器内的图片来调整布局

有时候一个商品卡片,可能有图片也可能只有文字。对于有图片的卡片,我们希望图片占左,文字占右;没有图片时,文字居中。传统做法是给卡片添加一个 class,比如 has-image。现在可以这样:

<div class="product-card">
    <img src="thumb.jpg">
    <div class="info">商品信息</div>
</div>
.product-card { display: flex; }
.product-card:has(> img) { flex-direction: row; }
.product-card:not(:has(> img)) { flex-direction: column; text-align: center; }

这里用了 > 直接子代选择器,意思是“存在一个img子元素”。:not(:has(…)) 就是没有图片的情况。这样你不用在模板里去条件判断要不要加class,CSS自己就能区分。

实战场景六:根据子元素数量改变样式

有同学会遇到一个需求:列表中的项少于3个时,让列表横向排列;超过3个时,让列表垂直排列。用 :has() 配合 :nth-child 或者 :nth-last-child 可以取巧:

<ul class="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
.list { display: grid; }
/* 如果第一个元素是最后一个元素(即只有一个子元素) */
.list:has(li:first-child:last-child) { grid-template-columns: 1; }
/* 如果第二个元素是最后一个元素(即只有两个子元素) */
.list:has(li:nth-child(2):last-child) { grid-template-columns: repeat(2, 1fr); }
/* 三个或更多使用默认grid */

很巧妙吧。 :has() 里面可以写组合选择器,这个表达能力非常强。

兼容性到底怎么样

我查了一下浏览器支持情况:Chrome 105+、Edge 105+、Safari 15.4+、Firefox 121+ 都已经完全支持 :has()。也就是说,从2023年底开始,主流浏览器已经可以放心使用了。如果你不需要兼容旧版本,那就可以直接把项目里的 :has() 用起来。

如果有兼容需求,可以用 @supports selector(:has(*)) 来判断:

@supports selector(:has(*)) {
    .field:has(input:placeholder-shown) .clear-btn { display: none; }
}

这样老浏览器不会解析里面的样式,不会导致界面异常。但老浏览器功能就缺失了,只能靠JS做回退。

再聊聊和 :has() 不能做的

虽然 :has() 很强,但它不能实现“兄弟元素影响前一个兄弟”的完全回溯,因为CSS的级联逻辑是单向的,但 :has() 提供了“选择祖先或兄长的能力”。比如你想让“前面的元素”根据“后面的元素”变化,它是不行的。比如: h1 + p:has(span) 可以,因为你选择的是p,而不是h1。你无法让h1根据后面p的状态来变化,因为 :has() 只能作用于你选中的那个元素本身,而那个元素在后面,它无法回头去改前面的h1。

举个例子: p:has(+ h1) 表示“后面有h1的p”,这个可以选中前面的p。但是 h1:has(~ p) 则表示“后面有p的h1”,所以h1就能知道后面是否有p。其实这已经算能影响祖先了,只是要正确选择目标元素。

实际项目中的一个优化:减少重复class

以前我们做列表页,每个卡片根据状态不同要加几个不同的class,比如“selected”、“disabled”、“warning”等等。用了 :has() 后,很多状态可以通过内部元素去判断,比如卡片里有一个checkbox,选中了就自动高亮卡片,就不需要额外的class。代码少了很多,而且当你用框架渲染列表时,不需要在模板里写三元表达式,只要把元素结构固定好就行。

我有一个同事刚开始对 :has() 很排斥,觉得这是“非主流”,在他见过我在表单校验上的用法之后,他也开始在自己的项目里用了。只要小心点用,这绝对是一个提高编写效率的好东西。

几个能用在真实项目里的完整小例子

例子1:下拉菜单的展开状态

用checkbox或者button,轻轻一点,靠 :has() 控制菜单的显示隐藏。

<div class="dropdown">
    <button class="trigger">点击展开</button>
    <div class="menu">这里是菜单内容</div>
</div>
.menu { display: none; }
.dropdown:has(.trigger:active) .menu { display: block; }

但注意 :active 只有在按住鼠标时才激活,实际使用可能会松手就消失。所以还是建议用checkbox或popover属性,这里只是演示 :has() 的用途。

例子2:检测一个区域是否有滚动条并添加阴影

.scroll-box:has(> .inner) { overflow: auto; }

这个意义不大,不过说明可以用于各种结构判断。

写到最后

CSS :has() 已经火了好几年,然而身边还是有很多人不知道或者不敢用。我建议大家在个人项目里多尝试,等试过几个场景后,你就会发现以前很多JS交互本来就是CSS该干的活。最后提醒一句,:has() 会消耗一点性能,在超大页面中不要写太复杂的嵌套关系,但正常规模项目完全没问题。

好了,这篇关于 :has() 的实战分享就到这里。下次你再遇到“根据元素内部状态改变自身样式”的需求,先想想用 :has() 是不是就够了。

CSS :has() 选择器终于能用了?这几个实战场景让我删掉大量JS
收藏 (0) 打赏

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

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

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

淘吗网 css CSS :has() 选择器终于能用了?这几个实战场景让我删掉大量JS https://www.taomawang.com/web/css/2577.html

常见问题

相关文章

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

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