CSS :has() 选择器:彻底改变 CSS 的父选择器终于来了

2534 字
13 分钟
CSS :has() 选择器:彻底改变 CSS 的父选择器终于来了

前言:CSS 选择器的百年孤独#

在 CSS 的世界里,选择器的方向一直是从上到下的——你可以选择一个元素的子元素、后代元素、相邻兄弟元素,但你无法根据子元素的状态来选择父元素

这是 CSS 社区呼声最高、等待最久的特性之一。开发者们年复一年地问同一个问题:

“我怎么选中『包含某个子元素』的父元素?”

答案曾经只有一个:用 JavaScript。

// 过去的做法:用 JS 给父元素加类
document.querySelectorAll('input:invalid').forEach((input) => {
input.closest('.form-group').classList.add('has-error');
});

现在,CSS :has() 选择器让这一切成为历史。它是一个关系型伪类选择器,允许你根据元素的后代、子元素甚至后续兄弟元素的状态来匹配该元素。

() 基础语法#

基本用法#

:has() 接受一个或多个选择器列表作为参数:

/* 选中"包含 <img> 子元素"的 .card */
.card:has(img) {
display: grid;
grid-template-rows: auto 1fr;
}
/* 选中"不包含 <img> 的 .card" */
.card:not(:has(img)) {
padding: 2rem;
}

:has() 的参数使用的是相对选择器——以被匹配的元素为起点进行匹配。

它不仅仅是”父选择器”#

虽然大家习惯叫它”父选择器”,但 :has() 的能力远不止于此。它可以根据任何关系来匹配元素:

/* 父选择器:选中包含 .active 子元素的 nav */
nav:has(.active) {
border-bottom: 3px solid blue;
}
/* 后续兄弟关系:选中后面紧跟着 p 的 h2 */
h2:has(+ p) {
margin-bottom: 0.5rem;
}
/* 通用兄弟关系:选中后面某处有 .error 的 label */
label:has(~ .error) {
color: red;
}
/* 后代关系:选中包含 checked 的 checkbox 的 form */
form:has(input[type="checkbox"]:checked) {
background: #f0f9ff;
}

() 中的组合选择器#

你可以在 :has() 中使用任何有效的选择器:

/* 包含直接子元素 img 的 .card */
.card:has(> img) {
/* 只匹配直接子元素,不匹配后代 */
}
/* 包含 hover 状态链接的 .card */
.card:has(a:hover) {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 包含至少 3 个 li 的 ul */
ul:has(li:nth-child(3)) {
columns: 2;
}
/* 多条件:同时包含 img 和 .badge 的 .card */
.card:has(img):has(.badge) {
position: relative;
}
/* 或条件:包含 img 或 video 的 .card */
.card:has(img, video) {
aspect-ratio: 16 / 9;
}

实战用例#

用例 1:表单验证样式#

这是 :has() 最经典的应用场景之一——根据输入框的验证状态来改变整个表单组的样式:

<form class="form">
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" required />
<span class="form-hint">请输入有效的邮箱地址</span>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" required minlength="8" />
<span class="form-hint">至少 8 个字符</span>
</div>
<button type="submit">提交</button>
</form>
/* 基础样式 */
.form-group {
margin-bottom: 1.5rem;
padding: 1rem;
border-radius: 8px;
border: 2px solid transparent;
transition: all 0.2s ease;
}
.form-hint {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
/* 输入框获得焦点时,高亮整个 form-group */
.form-group:has(input:focus) {
border-color: #3b82f6;
background: #eff6ff;
}
.form-group:has(input:focus) label {
color: #1d4ed8;
font-weight: 600;
}
/* 输入无效时(用户已交互) */
.form-group:has(input:not(:placeholder-shown):invalid) {
border-color: #ef4444;
background: #fef2f2;
}
.form-group:has(input:not(:placeholder-shown):invalid) label {
color: #dc2626;
}
.form-group:has(input:not(:placeholder-shown):invalid) .form-hint {
color: #dc2626;
}
/* 输入有效时 */
.form-group:has(input:not(:placeholder-shown):valid) {
border-color: #22c55e;
background: #f0fdf4;
}
.form-group:has(input:not(:placeholder-shown):valid) label {
color: #16a34a;
}
/* 表单中有任何无效输入时,禁用提交按钮的视觉 */
.form:has(input:invalid) button[type="submit"] {
opacity: 0.5;
cursor: not-allowed;
}
/* 所有输入都有效时,提交按钮高亮 */
.form:has(input:valid):not(:has(input:invalid)) button[type="submit"] {
background: #22c55e;
color: white;
transform: scale(1.02);
}

零 JavaScript! 整个表单验证的视觉反馈完全由 CSS 驱动。

用例 2:动态网格布局#

根据内容自动调整布局策略:

/* 基础网格 */
.grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
/* 当网格中有"特色"卡片时,切换为更灵活的布局 */
.grid:has(.card--featured) {
grid-template-columns: 2fr 1fr 1fr;
grid-template-rows: auto;
}
/* 特色卡片跨两行 */
.grid:has(.card--featured) .card--featured {
grid-row: span 2;
}
/* 当网格为空时显示空状态 */
.grid:not(:has(.card)) {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
.grid:not(:has(.card))::after {
content: '暂无内容';
color: #9ca3af;
font-size: 1.25rem;
}

用例 3:智能导航高亮#

/* 当导航中有活跃链接时,添加底部指示条 */
nav:has(.nav-link.active) {
border-bottom: 3px solid #3b82f6;
}
/* 当下拉菜单打开时(包含 :focus-within 或 [open]) */
.nav-item:has(.dropdown[open]) {
background: #f3f4f6;
border-radius: 8px;
}
/* 当下拉菜单打开时,调暗其他导航项 */
nav:has(.dropdown[open]) .nav-item:not(:has(.dropdown[open])) {
opacity: 0.6;
}

用例 4:根据图片方向调整卡片布局#

/* 包含横向图片的卡片 */
.card:has(img[data-orientation="landscape"]) {
grid-template-rows: 200px auto;
}
.card:has(img[data-orientation="landscape"]) img {
width: 100%;
object-fit: cover;
}
/* 包含纵向图片的卡片 */
.card:has(img[data-orientation="portrait"]) {
grid-template-columns: 150px 1fr;
}
.card:has(img[data-orientation="portrait"]) img {
height: 100%;
object-fit: cover;
}
/* 没有图片的卡片 */
.card:not(:has(img)) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
}

用例 5:主题/模式切换#

根据表单控件状态切换整个页面主题——无需 JavaScript:

<div class="theme-controller">
<input type="checkbox" id="dark-mode" hidden />
<label for="dark-mode" class="theme-toggle">🌙 暗色模式</label>
<div class="page-content">
<!-- 页面所有内容 -->
</div>
</div>
.theme-controller:has(#dark-mode:checked) .page-content {
--bg: #1a1a2e;
--text: #e0e0e0;
--card-bg: #16213e;
--border: #2a2a4a;
background: var(--bg);
color: var(--text);
}
.theme-controller:has(#dark-mode:checked) .card {
background: var(--card-bg);
border-color: var(--border);
}
.theme-controller:has(#dark-mode:checked) .theme-toggle {
background: #334155;
color: #fbbf24;
}
.theme-controller:has(#dark-mode:checked) .theme-toggle::before {
content: '☀️ 亮色模式';
}

配合 () 和 () 组合使用#

:has():is():where() 组合使用,可以写出极其强大且简洁的选择器。

() 简化多选择器#

/* 不用 :is(),要写很多重复代码 */
h1:has(+ p),
h2:has(+ p),
h3:has(+ p),
h4:has(+ p) {
margin-bottom: 0.5rem;
}
/* 用 :is() 简化 */
:is(h1, h2, h3, h4):has(+ p) {
margin-bottom: 0.5rem;
}
/* 包含任意交互元素的容器 */
.container:has(:is(a, button, input, select, textarea):focus-visible) {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}

() 零优先级组合#

:where() 的优先级始终为 0,适合用来写可覆盖的基础样式:

/* 基础样式:优先级为 0,容易被覆盖 */
:where(.card):has(img) {
display: grid;
grid-template-rows: auto 1fr;
}
:where(.form-group):has(input:invalid) {
border-color: red;
}
/* 业务样式:正常优先级,可以覆盖上面的规则 */
.card.card--compact:has(img) {
grid-template-columns: 80px 1fr;
grid-template-rows: auto;
}

复杂组合实战#

/* 当文章中包含代码块时,调整排版 */
article:has(:is(pre, code)) {
--content-max-width: 80ch;
}
/* 当表格在窄容器中时(结合 container query 和 :has) */
.table-wrapper:has(table:is([data-cols="5"], [data-cols="6"], [data-cols="7"])) {
overflow-x: auto;
}
/* 侧边栏有内容时,主区域调整宽度 */
.layout:has(:is(.sidebar):not(:empty)) .main {
max-width: 70%;
}
.layout:has(:is(.sidebar):empty) .main,
.layout:not(:has(.sidebar)) .main {
max-width: 100%;
}

高级技巧#

量词选择器模式#

利用 :has():nth-child()/:nth-last-child() 实现”根据子元素数量”应用样式:

/* 只有 1 个子元素 */
.grid:has(> :nth-child(1)):not(:has(> :nth-child(2))) {
grid-template-columns: 1fr;
max-width: 600px;
margin: 0 auto;
}
/* 恰好 2 个子元素 */
.grid:has(> :nth-child(2)):not(:has(> :nth-child(3))) {
grid-template-columns: 1fr 1fr;
}
/* 3 个或更多子元素 */
.grid:has(> :nth-child(3)) {
grid-template-columns: repeat(3, 1fr);
}
/* 6 个或更多 */
.grid:has(> :nth-child(6)) {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

这让布局可以根据内容数量自动调整,不需要 JavaScript 计算。

状态机模式#

用隐藏的 checkbox/radio 配合 :has() 实现纯 CSS 状态管理:

<div class="tabs-container">
<input type="radio" name="tab" id="tab-1" checked hidden />
<input type="radio" name="tab" id="tab-2" hidden />
<input type="radio" name="tab" id="tab-3" hidden />
<nav class="tabs-nav">
<label for="tab-1" class="tab-btn">概览</label>
<label for="tab-2" class="tab-btn">详情</label>
<label for="tab-3" class="tab-btn">评论</label>
</nav>
<div class="tab-panel" id="panel-1">概览内容...</div>
<div class="tab-panel" id="panel-2">详情内容...</div>
<div class="tab-panel" id="panel-3">评论内容...</div>
</div>
.tab-panel {
display: none;
}
/* 根据选中的 radio 显示对应面板 */
.tabs-container:has(#tab-1:checked) #panel-1,
.tabs-container:has(#tab-2:checked) #panel-2,
.tabs-container:has(#tab-3:checked) #panel-3 {
display: block;
}
/* 高亮选中的标签 */
.tabs-container:has(#tab-1:checked) label[for="tab-1"],
.tabs-container:has(#tab-2:checked) label[for="tab-2"],
.tabs-container:has(#tab-3:checked) label[for="tab-3"] {
color: #3b82f6;
border-bottom: 2px solid #3b82f6;
font-weight: 600;
}

完全无 JavaScript 的 Tab 切换组件。

全局上下文感知#

:has() 可以应用在 htmlbody 上,实现全局感知:

/* 当页面有模态框打开时 */
body:has(.modal.is-open) {
overflow: hidden;
}
body:has(.modal.is-open) .page-content {
filter: blur(4px);
pointer-events: none;
}
/* 当页面有视频播放时 */
body:has(video:not([paused])) .floating-player {
display: block;
}
/* 当页面有选中的商品时,显示底部操作栏 */
body:has(.product-item input:checked) .batch-actions {
transform: translateY(0);
opacity: 1;
}

性能注意事项#

:has() 选择器的性能与其复杂度密切相关。浏览器需要从被匹配的元素开始,向下查找是否存在符合条件的后代——这与传统选择器的匹配方向相反。

性能建议#

/* ✅ 好:限定了直接子元素 */
.card:has(> img) { }
/* ✅ 好:选择器简单 */
.form:has(:invalid) { }
/* ⚠️ 注意:后代选择器,匹配范围大 */
.page:has(.deep .nested .element) { }
/* ❌ 避免:在通配符或大范围元素上使用复杂 :has() */
*:has(.something) { }
div:has(span:nth-child(odd):not(.excluded):hover) { }

最佳实践#

  1. 尽量缩小匹配范围:用 > 直接子选择器而非后代选择器
  2. 避免在高频变化的元素上使用复杂 :has():如 :hover 触发的大范围重新匹配
  3. 使用具体的类名而非标签选择器
  4. 测试性能:在目标浏览器中实际测量渲染性能

浏览器兼容性#

截至 2026 年,:has() 的支持情况非常好:

浏览器版本
Chrome105+ ✅
Firefox121+ ✅
Safari15.4+ ✅
Edge105+ ✅

全球覆盖率已超过 93%,可以放心在生产环境中使用。

对于需要兼容旧浏览器的场景:

/* 渐进增强 */
.form-group {
border-color: #e5e7eb; /* 默认 */
}
/* 支持 :has() 的浏览器 */
@supports selector(:has(*)) {
.form-group:has(input:focus) {
border-color: #3b82f6;
}
}

与 JavaScript 的边界#

虽然 :has() 极大地减少了对 JavaScript 的依赖,但它并不能完全替代 JS。以下是合理的分工:

任务CSS ()JavaScript
根据子元素状态改变父元素样式不再需要
表单验证视觉反馈仍需处理提交逻辑
根据内容数量调整布局不再需要
动态增删 DOM 元素仍然需要
异步数据获取和渲染仍然需要
复杂状态管理有限更合适

原则:样式逻辑归 CSS,行为逻辑归 JavaScript。:has() 让样式逻辑更强大了,但不要试图用它替代所有 JS。

总结#

CSS :has() 选择器是 CSS 选择器体系中缺失已久的最后一块拼图。它的出现:

  • 消除了”父选择器”的空白:终于可以根据子元素状态选择父元素
  • 大幅减少 JS 的样式操作:表单验证、状态切换等场景不再需要 JS
  • 解锁了全新的 CSS 模式:量词选择器、全局上下文感知、纯 CSS 状态机
  • ()/() 组合:构成了现代 CSS 选择器的三驾马车

:has() 不仅仅是一个新选择器——它改变了我们思考 CSS 的方式。从”自上而下”的单向选择,到”双向感知”的智能匹配。如果你还没有开始使用它,现在正是时候。

记住:CSS 能做的事情,就让 CSS 来做。 :has() 让 CSS 能做的事情又多了一大步。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CSS :has() 选择器:彻底改变 CSS 的父选择器终于来了
https://boke.hackerdream.xyz/posts/css-has-selector/
作者
晴天
发布于
2026-01-02
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录