父子样式总冲突,has伪类咋样破局

3,380字
14–21 分钟
in

在网页开发中,经常碰到想给父容器加个背景,但前提是容器里面得包含某个特定子元素。以前只能靠JavaScript一顿操作,现在CSS出了个叫has伪类的新家伙,直接能拿捏这种场景。has伪类就像个探测器,瞅一眼元素内部有没有指定内容,有的话就给父元素上样式。这篇东西就掰扯掰扯has伪类到底咋用,顺带对比老方法和新套路,让样式冲突不再翻车。

目录

啥是has伪类

has伪类是CSS里一种函数式伪类,写法是:has(选择器)。它的作用是:当某个元素内部(或者后续位置)存在符合括号里选择器的其他元素时,就选中这个元素。可以理解为反向操作,普通CSS只能从父到子,has伪类允许从子到父或者从后到前。比如一个卡片组件,里面如果包含图片,想让整个卡片边框变色,用has伪类就能轻松搞定。这玩意儿在2022年以后被主流浏览器陆续支持,前端圈子里很快就火起来,因为以前做不到的事现在纯CSS就能整。

传统JS咋整

过去没有has伪类的日子,要检测一个元素后面是不是跟着另一个特定元素,得写JavaScript来遍历DOM。假设一个博客列表页,每个文章标题是<h1>,如果这个<h1>后面紧跟着一个<h2>副标题,想让标题文字变蓝突出显示。老派做法是这样:

const allHeadings = document.querySelectorAll('h1');
allHeadings.forEach((heading) => {
  const nextElement = heading.nextElementSibling;
  if (nextElement && nextElement.tagName.toLowerCase() === 'h2') {
    heading.style.color = 'blue';
  }
});

这段代码先抓取页面上所有的<h1>,挨个检查它的下一个兄弟元素是不是<h2>。如果是,就手动改颜色。这里有个坑:如果页面内容是通过Ajax动态加载的,这段JS必须在每次加载后重新跑一遍。另外,如果需求变成“检测内部任意位置有没有某元素”,JS写起来就更复杂,可能需要递归查询。而且大量DOM操作在低端手机上可能会卡顿。用JS虽然能解决问题,但总归多写不少代码,还得操心执行时机。

CSS新方案

有了has伪类,同样的事情一行CSS就能搞定。还是上面的例子,想要选中“后面紧跟着<h2>”的<h1>,直接这样写:

h1:has(+ h2) {
  color: blue;
}

这里的+是相邻兄弟选择器,h1:has(+ h2)的意思是:如果某个<h1>后面紧挨着一个<h2>,那么这个<h1>就应用蓝色文字。相比JS方案,CSS方案完全不需要操心动态内容,因为样式是实时响应的,新加进来的DOM元素只要符合条件,样式自动生效。下面拆解一个更完整的实操流程,确保跟着做就能跑起来。

流程一:检测父容器内是否有某子元素

场景:一个图片展示区,用了<figure>标签,里面可能包含<figcaption>图片说明。如果存在说明文字,想让整个<figure>有个浅灰背景和圆角。步骤:

  1. 写HTML结构,准备两个<figure>,一个有说明,一个没有:
<div class="gallery">
  <figure>
    <img src="https://placedog.net/300/200" alt="小狗照片">
    <figcaption>这是王阿姨家的柯基</figcaption>
  </figure>
  <figure>
    <img src="https://placedog.net/300/200" alt="另一只小狗">
  </figure>
</div>
  1. 在样式表里添加has伪类规则,检测<figure>内部是否有<figcaption>
figure:has(figcaption) {
  background: #f0f0f0;
  padding: 12px;
  border-radius: 8px;
  max-width: 320px;
}
  1. 打开浏览器查看效果:第一个<figure>因为里面有说明文字,所以背景变灰且有圆角;第二个<figure>没有说明,保持原样。

这个流程里要注意一点:has伪类括号里的选择器可以是任意复杂的选择器,比如figure:has(img[alt*="狗"])能筛选出内部图片alt属性包含“狗”字的<figure>。但是别尝试嵌套has,比如:has(:has(...)),浏览器不支持这种写法,会整条样式失效。另外,伪元素(::before::after)也不能放在has里面当条件,写了也没效果。

流程二:检测表单里是否有必填项或错误提示

实际开发中,一个表单区块如果包含必填项(required属性)或者有错误提示信息,想给这个区块加个红边框提醒用户。具体步骤:

  1. 构建一个表单区域,包含普通输入框和一个必填输入框:
<div class="form-section">
  <label>用户名</label>
  <input type="text" name="username">
</div>
<div class="form-section">
  <label>邮箱(必填)</label>
  <input type="email" name="email" required>
  <span class="error-msg" style="display:none;">邮箱格式不对</span>
</div>
  1. 用has伪类检测.form-section内部是否存在input:required
.form-section:has(input:required) {
  border-left: 4px solid orange;
  background-color: #fff8e7;
}
  1. 如果想更进一步,检测到错误提示显示的时候给更强烈的红框,可以写:
.form-section:has(.error-msg:not(:hidden)) {
  border-left: 4px solid red;
  background-color: #ffe6e6;
}

注意这里用了:not(:hidden)来检测错误信息是否可见。动态显示错误提示时,样式会实时变化。有个实战小坑:如果表单区块里的错误信息是通过JS动态添加的,has伪类照样能捕获到,因为CSS选择器实时匹配。但要注意,has伪类本身在老旧浏览器(比如Firefox 121以下版本)不识别,如果必须兼容那些浏览器,整条规则会被忽略。可以用@supports做降级处理:

@supports selector(:has(*)) {
  .form-section:has(input:required) {
    /* 只有支持has的浏览器才生效 */
  }
}

这样在不支持has的浏览器里,不会因为未知选择器导致整个样式表报错。

哪些能用

has伪类现在已经得到绝大多数现代浏览器的认可,具体支持版本如下:

浏览器最低版本
Chrome105
Edge105
Safari15.4
Firefox121
安卓Chrome146
iOS Safari15.4

在项目里放心用,除非需要照顾IE这种古董。日常开发中,搭配@supports做渐进增强,既能给新浏览器更好的体验,又不让老浏览器翻车。

实战小例子

社区里有大佬用has伪类做键盘导航的状态检测。比如一个树形组件,需要判断某个节点是否有.focus-visible类的兄弟元素,从而调整父级样式。写法类似:

.tree-node:has(+ .focus-visible) {
  outline: 2px solid #0a5;
}

另外处理第三方图标库的SVG时,如果某个SVG内部包含特定ID的元素,也能用has定向修改描边粗细。比如一个邮件图标库,里面有个#Mail元素,想给外层SVG加粗描边:

svg:has(> #Mail) {
  stroke-width: 2;
}

注意这里用了子代选择器>,精确匹配直接子元素。如果ID是动态生成的,也可以用属性选择器svg:has([id*="Mail"])。还有一点:has伪类的优先级由括号里最具体的选择器决定,比如div:has(#specialId)的优先级相当于div加上#specialId的权重,比普通的div高很多,写的时候要心里有数。

搞完这些套路,再碰到父子样式纠缠的需求,第一反应就不是撸JS了,而是掏出has伪类试试。收工!