终于,在规范讨论许久之后,现代浏览器开始支持CSS :has()伪类了!这无疑是CSS领域一个激动人心的里程碑。
:has()伪类的强大程度超乎想象,它让许多以往必须修改DOM结构或依赖JavaScript才能实现的功能,现在仅用纯CSS即可完成。这篇文章将通过几个具体的实战案例,带你领略它的魅力。
兼容性提醒::has()需要Chrome 101+(105+正式支持)、Safari 15.4+或开启了实验特性的Firefox。对于可以指定Chrome内核版本的Electron应用或内部项目,现在就可以尝试使用了。
一、:has伪类基础语法
:has()的语法非常直观,它允许你根据其后代或后续兄弟元素的状态来匹配当前元素。
例如,下面的选择器只会匹配那些直接包含 <img> 子元素的 <a> 标签:
a:has(> img)
而下面的选择器,则只会匹配其后紧跟着 <p> 元素的 <h1> 标题:
h1:has(+ p)
从形式上看,去除 :has() 后,括号内的选择器本身是完整的(例如 a > img)。而加上 :has() 后,我们能够选中这个完整选择器最前面的那个元素。
理论稍显抽象,接下来让我们通过实例来感受它的强大。
二、为表单必填项自动添加标记
一个常见的表单需求:在必填项的标签前添加红色星号 *。
假设我们有如下HTML结构:
<form>
<item>
<label>用户名</label>
<input required>
</item>
<item>
<label>备注</label>
<input>
</item>
</form>

过去,我们可能需要手动添加类名或调整HTML结构。现在,借助 :has(),一行CSS就能解决:
label:has(+input:required)::before{
content: '*';
color: red;
}
这段代码的意思是:选中那些紧随其后的 input 拥有 :required 属性的 label 元素,并为其添加一个红色的星号伪元素。

三、实现精确的拖拽手柄区域
在列表拖拽交互中,为了不影响列表项内部的其他操作,我们通常希望只有特定区域(如一个手柄)可以触发拖拽。
期望效果是:鼠标悬停在列表项上时,才显示拖拽手柄,只有按住手柄才能拖拽。

HTML结构如下:
<div class="content">
<div class="item">列表<span class="thumb"></span></div>
<div class="item">列表<span class="thumb"></span></div>
<div class="item">列表<span class="thumb"></span></div>
</div>
关键CSS实现:
.thumb{
/**/
opacity: 0
}
.item:hover .thumb{
opacity: 1;
}
.item:has(.thumb:hover){
-webkit-user-drag: element;
}
逻辑很清晰:
- 默认隐藏手柄(
.thumb)。
- 悬停列表项时显示手柄。
- 核心:当手柄(
.thumb)被悬停时,通过 :has(.thumb:hover) 选中其父级 .item,并为该列表项添加可拖拽属性。

四、解决多层嵌套结构的Hover冒泡问题
考虑一个三层嵌套的DOM结构:
<div class="box-1">
<div class="box-2">
<div class="box-3"></div>
</div>
</div>
如果直接为所有 div 添加 :hover 样式:
div:hover{
outline:4px dashed rebeccapurple
}
你会发现,当鼠标悬停在最内层的 box-3 时,外层所有的 div 都会触发 hover 样式,效果如同JavaScript的事件冒泡。

如何让 hover 效果只作用于当前最内层的元素呢?:has() 提供了一个巧妙的解决方案:
div:not(:has(:hover)):hover{
outline:4px dashed rebeccapurple
}
我们来拆解一下这个选择器:
div:has(:hover):匹配其子元素正处于 :hover 状态的 div(即当前悬停元素的父级们)。
:not(:has(:hover)):反过来,匹配那些没有子元素处于 :hover 状态的 div(即当前悬停元素本身)。
- 再结合外层的
:hover,最终效果就是:只有鼠标直接悬停的那个最内层 div 会显示边框。

这个技巧在复杂的可视化拖拽平台或嵌套组件中非常有用。
五、构建纯CSS五星评分组件
评分组件是前端常见的交互元素。过去实现“高亮当前星及之前的所有星”的交互,往往需要调整DOM顺序或借助JS。现在,:has() 让纯CSS实现变得异常简单。
HTML结构(5个单选按钮):
<star>
<input name="star" type="radio">
<input name="star" type="radio">
<input name="star" type="radio">
<input name="star" type="radio">
<input name="star" type="radio">
</star>
经过基础样式修饰(使用SVG Mask生成心形),初始状态如下:

实现交互的核心CSS:
star [type="radio"]:hover,
star [type="radio"]:has(~:hover),
star:not(:hover) [type="radio"]:checked,
star:not(:hover) [type="radio"]:has(~:checked){
background: orangered;
}
[type="radio"]:hover:匹配当前悬停的星星。
[type="radio"]:has(~:hover):关键! 匹配位于当前悬停星星之前的所有星星。
- 后两条规则同理,用于处理点击选中(
:checked)后的状态。
这样,无论是鼠标滑过还是点击选中,都能实现连贯的高亮效果。

六、打造日期范围选择器(CSS为主,JS为辅)
日期范围选择是:has()大显身手的重磅场景。我们将实现:选择两个日期后,自动高亮它们之间的所有日期;在只选中一个日期时,鼠标滑过可预览选择范围。
假设日历HTML由一系列 span 组成:
<div class="date">
<span>1</span>
<span>2</span>
<span>3</span>
...
<span>30</span>
<span>31</span>
</div>
1. 静态范围高亮
当两个日期被添加 .select 类后,如何高亮它们之间的区域?
.select,
.select~span:has(~.select){
background-color: blueviolet;
color: #fff;
}
.select:选中第一个和最后一个日期。
.select~span:has(~.select):选中位于第一个 .select 之后,并且在其后面还存在第二个 .select 的所有 span。这完美地匹配了两个选中日期之间的所有元素。

2. 动态Hover预览
在已选中一个日期(.select)后,鼠标滑过时预览从已选日期到当前悬停日期的范围。这需要判断鼠标在已选日期之前还是之后:
span:hover~span:has(~.select),
.select~span:has(~:hover)
{
background-color: blueviolet;
color: #fff;
}
span:hover~span:has(~.select):当鼠标悬停点在已选日期之前时,匹配从悬停点之后到已选日期之前的所有日期。
.select~span:has(~:hover):当鼠标悬停点在已选日期之后时,匹配从已选日期之后到悬停点之前的所有日期。

3. 区分“单选”与“范围已确定”状态
当两个日期都被选中后,应结束预览状态。我们可以用 :has() 检测是否存在两个 .select 元素:
.date:not(:has(.select~.select)) .select,
.date:not(:has(.select~.select)) span:hover{
/* 当没有两个.select时,给选中和悬停的日期添加轮廓线样式 */
outline: 2px solid blueviolet;
}
.date:not(:has(.select~.select)) span:hover~span:has(~.select),
.date:not(:has(.select~.select)) .select~span:has(~:hover)
{
/* 当没有两个.select时,应用范围预览的高亮样式 */
background-color: blueviolet;
}
.select~.select 表示“选中的 .select 元素后面还有另一个 .select ”。通过 :has(.select~.select) 就能判断是否已经完成范围选择。
至此,绝大部分视觉逻辑已由CSS完成,JavaScript只需充当简单的“状态记录器”:
date.addEventListener('click', ev => {
const current = date.querySelectorAll('.select');
if (current.length == 2) {
current.forEach(el => {
el.classList.remove('select')
})
}
ev.target.classList.add('select')
})
最终效果如下,一个以CSS为核心、交互逻辑清晰的日期范围选择器就完成了:

总结与展望
通过以上六个案例,我们看到了CSS :has() 伪类如何革新前端交互的实现方式。它彻底打破了选择器只能“向下”或“向后”选择的限制,让我们能够基于子元素或兄弟元素的状态来逆向选择父元素或前面的元素。
这意味着:
- DOM结构将更加语义化,无需再为了样式而妥协。
- 大量交互视觉逻辑可以回归CSS,JavaScript得以更专注于数据与业务逻辑。
- 以前诸多需要“奇技淫巧”才能实现的布局与交互,现在都有了简洁优雅的解决方案。
虽然全面兼容仍需时日,但在可控环境(如内部系统、Electron应用)中已经可以率先体验。:has() 伪类的到来,标志着CSS正在朝着更强大、更声明式的方向迈进,它将极大丰富前端开发者的样式表达能力。
准备好迎接这个未来了吗?欢迎在云栈社区分享你的 :has() 实践心得。