用了十几年 CSS,我们一直在抱怨同一件事:只能从父选 子,不能从子反选父。直到
:has()出现,这条祖训被彻底打破。
一、那个困扰前端十年的难题
先来感受一下痛点。假设你要实现:当一个卡片里有图片时,给卡片加上边框。
过去你只能这样写 JS:
document.querySelectorAll('.card').forEach(card => {
if (card.querySelector('img')) {
card.classList.add('has-image');
}
});
绑定、判断、加类名……一套流程下来十几行。而现在,一行 CSS 搞定:
.card:has(img) {
border: 2px solid #0052d9;
}
翻译成人话就是:「选中那些内部包含 img 的 .card」。:has() 让选择器第一次拥有了「向上看」的能力。
二、:has() 到底怎么读?
记住一个万能句式:
A:has(B) = 「包含 B 的 A」
注意,最终被选中、被加样式的是 A(父级),而不是 B。这正是它颠覆性的地方——CSS 史上第一个真正的「父选择器」。
/* 包含被选中复选框的 label */
label:has(input:checked) { color: green; }
/* 后面紧跟着 p 的 h2 */
h2:has(+ p) { margin-bottom: 0; }
/* 不包含图片的文章 */
article:not(:has(img)) { padding: 20px; }
它还能和 +、~、> 等组合器自由搭配,表达「兄弟关系」「相邻关系」,灵活得超乎想象。
三、16 个让你直呼「真香」的实战场景
场景 1:表单校验提示
输入框为空且失焦后,让整个表单组变红:
.field:has(input:invalid) {
border-color: #e34d59;
}
.field:has(input:invalid) .tip {
display: block; /* 显示错误提示 */
}
以前要监听 blur、input 事件,现在 CSS 自己就能感知校验状态。
场景 2:图文混排自适应
文章里有图片就用两栏布局,没有就用单栏:
.post:has(img) { display: grid; grid-template-columns: 1fr 1fr; }
.post:not(:has(img)) { display: block; }
场景 3:下拉菜单展开态
.menu:has(.submenu:hover) {
background: #f5f7fa;
}
鼠标移到子菜单时,父菜单同步高亮,无需任何 JS。
场景 4:数量驱动的样式
配合 :nth-child 还能实现「当列表项超过 5 个时换个样式」这种过去想都不敢想的逻辑:
ul:has(li:nth-child(6)) {
columns: 2; /* 6 项及以上自动分两列 */
}
场景 5:弹窗打开锁住背景滚动
过去要靠 JS 给 body 加 overflow:hidden,现在一行 CSS 感知 dialog 状态:
body:has(dialog[open]) {
overflow: hidden;
}
场景 6:图片未加载时显示占位
figure:has(img:not([src])) {
background: #f5f7fa url('placeholder.svg') center / 48px no-repeat;
}
src 还没填上时显示占位图,加载完自动消失。
场景 7:行聚焦,hover 一行上方各行变暗
表格里鼠标移到某一行时,让它上面的所有兄弟行变淡,突出当前行:
.row:has(~ .row:hover) {
opacity: .5;
}
这种「兄弟联动」的效果,以前必须靠 JS 遍历计算,现在纯 CSS 就能优雅实现。
场景 8:步骤条自动标记已完成
后面存在 .active 步骤的,说明前面这些都已完成,自动标绿:
.step:has(~ .step.active) {
color: #00a870;
}
.step:has(~ .step.active)::before {
content: '✓';
}
场景 9:搜索框聚焦放大父容器
输入框获得焦点时,让整个搜索框容器「弹」起来:
.search-box:has(input:focus) {
transform: scale(1.02);
box-shadow: 0 4px 16px rgba(0,0,0,.1);
}
场景 10:空状态自动提示
列表里一个子项都没有时,自动显示「暂无数据」提示,再也不用 JS 判断 length === 0:
.list:not(:has(.item)) .empty-tip {
display: block;
}
场景 11:必填项自动标红星
表单组里含 required 输入框时,自动给 label 加上红色星号:
.field:has(input:required) label::after {
content: ' *';
color: #e34d59;
}
场景 12:一个开关切整页暗色
页面里的暗色开关被勾选时,整页切换为暗色主题,零 JS:
body:has(#dark-toggle:checked) {
background: #1a1a1a;
color: #eee;
}
场景 13:全选框联动
表格里所有行都勾选时,让表头高亮提示「已全选」:
.table:not(:has(tbody input:not(:checked))) thead {
background: #f0f6ff;
}
读法:「不存在未勾选的行」→ 说明全部勾选了。
注意:这种写法在表格一行都没有时也会命中(「不存在未勾选行」同样成立)。如果担心空表格被误高亮,再加一个「至少有一行」的条件即可:
.table:has(tbody input):not(:has(tbody input:not(:checked))) thead { background: #f0f6ff; }
场景 14:卡片选中态高亮
卡片内 radio 被选中时整张卡片高亮,套餐、支付方式选择常用:
.plan-card:has(input:checked) {
border-color: #0052d9;
background: #f0f6ff;
}
场景 15:图片加载失败兜底
图片加载出错时,在父容器层显示备用占位背景(配合 onerror 打上标记后一劳永逸):
figure:has(img.broken) {
background: #f5f7fa url('fallback.svg') center / 64px no-repeat;
}
场景 16:相邻标题自动压缩间距
h2 后紧跟着 h3 时压缩上下间距,纯排版优化,免手写一堆 margin 调整:
h2:has(+ h3) {
margin-bottom: 4px;
}
四、两条避坑提醒
1. 兼容性已经 OK 了。 截至 2026 年,Chrome、Safari、Firefox、Edge 全部支持 :has(),主流浏览器覆盖率超过 93%。生产环境放心用,老旧浏览器可用 @supports selector(:has(*)) 做降级。
@supports selector(:has(*)) {
.card:has(img) { border: 2px solid #0052d9; }
}
2. 别滥用导致性能问题。:has() 的匹配开销比普通选择器大,尤其是写出 *:has(...) 这种无限定范围的选择器时。给它一个明确的父级标签或类名,浏览器就能高效命中。
/* ❌ 范围太大,浏览器要遍历所有元素 */
*:has(> img) { ... }
/* ✅ 限定清晰,性能友好 */
.gallery:has(> img) { ... }
五、写在最后
:has() 的意义远不止「少写几行代码」。它把过去必须用 JS 处理的「状态联动」搬回了 CSS——更声明式、更易维护、性能也更好。
一句话总结:凡是「子元素的状态要影响父元素或兄弟元素样式」的需求,先想想能不能用 :has() 解决。 你会发现,项目里的很多 JS 其实都可以删掉了。
下次写交互样式时,不妨先问自己一句:「这个,:has() 能搞定吗?」





