引言
"组长,这个需求我写不了。"
"什么需求?"
"产品经理说,所有包含图片的卡片,要在卡片上加一个'带图标识'的边框。但是这些卡片是动态渲染的,图片可有可无,我总不能每个卡片都写个条件判断吧?"
组长瞥了我一眼:"你用 CSS 啊。"
"CSS 怎么选?CSS 又没办法判断一个元素里有没有图片......"
组长微微一笑:"那是以前的 CSS 了。你知道 :has() 吗?它能让父元素根据子元素的状态来改变自己。简单来说,就是 '子凭父贵'的反过来------父凭子贵。"
我当时一脸懵:还有这种操作?
那天下午,我学会了 :has(),然后发现------原来 CSS 早就不是当年的 CSS 了。它悄悄给自己装了个"逆向思维"的外挂,只是我们都不知道。
一、:has() 是什么?CSS 的"时光倒流"
在 CSS 选择器的历史上,我们一直只能从上往下选:父元素 → 子元素,兄弟元素 → 相邻兄弟。比如 div p 选择 div 里的所有 p,h1 + p 选择紧跟在 h1 后面的 p。
但从来没有人能根据子元素的状态来选择父元素 。直到 :has() 出现。
:has() 是一个关系伪类,它允许你根据元素的后代或后续兄弟元素来匹配该元素。语法看起来就像是在问:"嘿,这个元素里面有没有符合某个条件的子元素?"
css
/* 选择所有包含 <img> 元素的 <figure> */
figure:has(img) {
border: 2px solid gold;
}
/* 选择所有包含 .error-message 的表单 */
form:has(.error-message) {
border: 1px solid red;
background-color: #ffeeee;
}
更妙的是,:has() 里面可以写几乎任何复杂选择器,包括伪类、组合器,甚至可以嵌套 :has()。
二、实战:那些让你拍大腿的场景
2.1 场景一:包含图片的卡片加特殊样式
终于不用 JS 了!
html
<div class="card">
<h3>标题</h3>
<p>一些文字...</p>
<img src="photo.jpg" alt="配图">
</div>
<div class="card">
<h3>标题</h3>
<p>没有图片的卡片</p>
</div>
css
.card:has(img) {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-left: 4px solid #ff8800;
}
只有带图片的卡片才会获得橙色左边框,干净利落。
2.2 场景二:表单实时校验反馈(不用 JS 监听)
css
/* 如果有无效输入框,给表单加个红框 */
form:has(input:invalid) {
border: 2px solid red;
padding: 10px;
}
/* 如果有被选中的复选框,给父级加个标记 */
fieldset:has(input[type="checkbox"]:checked) {
background-color: #e0ffe0;
}
这比以前用 JS 监听每个 input 然后给父级加类名优雅太多。
2.3 场景三:空状态提示
css
/* 如果列表里没有 li,显示空状态提示 */
ul:not(:has(li))::after {
content: "暂无数据";
display: block;
color: #999;
text-align: center;
}
:not(:has(...)) 这个组合很有用,表示"没有子元素满足条件"。
2.4 场景四:兄弟元素的影响
:has() 不仅可以选祖先,还可以选兄弟?
css
/* 如果 h2 后面紧跟着 p,给 h2 加下划线 */
h2:has(+ p) {
text-decoration: underline;
}
这利用了 + 组合器,+ p 表示"后面紧邻的 p",所以 h2:has(+ p) 就是"后面有 p 的 h2"。实际上 :has() 里的选择器可以往后看。
2.5 场景五:多级嵌套的"父选择"
css
/* 如果某个 section 里有一个 article,且 article 内有 img,给 section 加背景 */
section:has(article:has(img)) {
background: #fafafa;
}
这就是嵌套 :has(),越看越像 XPath,但威力巨大。
三、:has() 的"阴暗面":性能与兼容
这么强大的东西,有没有什么坑?
3.1 兼容性
:has() 是 CSS 选择器 Level 4 的一部分。它在 Chrome 105+、Edge 105+、Firefox 121+、Safari 15.4+ 开始支持。也就是说,2023 年以后的主流浏览器基本都能用。但对于老浏览器,需要做降级处理(比如用 JS 回退)。
3.2 性能考虑
:has() 被称为"昂贵的选择器",因为它需要检查元素的后代或后续兄弟,浏览器可能需要做更多工作。但现代浏览器已经做了大量优化,在合理使用下不会明显影响性能。不要滥用 ,比如不要给每个元素都加上 :has(*) 这种通配。
最佳实践 :尽量限定范围,比如 nav:has(> a.active) 比 *:has(a) 高效得多。
3.3 一些你不能做(或不应做)的事
- 不能在
:has()里使用:has()自身形成循环引用?理论上可以,但你会把自己绕晕。 - 不能用
:has()选择祖先的祖先?它可以,但性能会下降。 - 不能用
:has()来改变页面结构?它只是选择器,只能应用样式,不能添加或删除元素。
四、还有哪些"逆天"的新选择器?
与 :has() 同期或稍早,CSS 还引入了:
:where():优先级为 0,用于降低选择器权重。:is():可以写一组选择器,比如:is(header, main, footer) p。:not()也升级了,可以接受复杂选择器列表。@scope实验性功能,可以限定样式的作用域。
这些新特性正在把 CSS 从"声明式样式表"变成"轻量级逻辑引擎"。
五、总结:CSS 不再是"语言残疾"
以前我们常开玩笑说:"CSS 不是编程语言。"现在,有了 :has(),CSS 居然能根据子元素来决定父元素样式,这几乎就是一种"条件判断"能力。
:has() 的出现,让我们可以少写很多 JavaScript 类名操作,让样式更纯粹、更内聚。虽然兼容性还没到 100%,但已经值得我们在现代项目中尝试。
下次产品经理再提"根据子元素内容改变父元素样式"的需求,你可以自信地说:"交给 CSS,不用写 JS。"
每日一问:你还遇到过哪些用 JS 实现很麻烦,但 CSS 新特性可以轻松解决的问题?评论区分享,一起刷新认知!