CSS :has() 入门教程:革新你的选择器能力
在 CSS 的世界里,选择器是我们定位和样式化 HTML 元素的核心工具。从简单的标签选择器 (div
) 到类选择器 (.my-class
)、ID 选择器 (#my-id
),再到属性选择器 ([type="text"]
) 和伪类 (:hover
, :focus
)、伪元素 (::before
, ::after
),CSS 提供了丰富的手段来描述我们想要样式化的目标。
然而,长期以来,CSS 选择器有一个著名的局限性:它们主要能根据元素的自身特性、子元素、后代元素、紧邻的同级元素(后一个)或后续的同级元素(后面的所有)来选择元素,但无法直接选择元素的父元素或之前的同级元素。我们常说“CSS 没有父选择器”。
这个限制在许多场景下都带来了不便。例如,你想给一个包含图片的 div
加上特殊的边框,或者当一个表单输入框变为无效状态时,改变其前一个兄弟元素(比如 label)的颜色。在过去,实现这些效果往往需要借助 JavaScript 来检查 DOM 结构并动态添加/移除类,或者编写冗长、脆弱的选择器组合,或者改变 HTML 结构。
但是,这一切随着 :has()
伪类选择器的出现而改变了。
:has()
是 CSS 选择器 Level 4 规范中引入的一个强大特性,它被誉为“CSS 的父选择器”,但其能力远不止于此。它允许你根据一个元素内部(或紧跟其后)的内容或状态来选择这个元素本身,甚至是它的祖先元素或之前的同级元素。这极大地扩展了 CSS 的表达能力,让许多原本需要 JavaScript 或繁琐变通方法才能实现的样式效果,现在仅凭 CSS 就能搞定。
本篇教程将带你深入了解 :has()
的语法、工作原理以及如何在实际项目中应用它, unlocking its full potential.
第一部分:认识 :has() – 语法与基本概念
1.1 :has() 的基本语法
:has()
伪类写在你想选择的元素选择器后面,括号内包含一个或多个相对选择器(relative selectors),用来描述你正在寻找的条件。
基本语法结构:
css
Selector:has(Relative_Selector) {
/* Styles to apply to Selector */
}
Selector
: 这是你想要选择的那个元素。可以是任何合法的 CSS 选择器,比如div
,.card
,#main
,article
,li
等。:has()
: 这是伪类本身。Relative_Selector
: 这是写在括号里的内容。它描述了一个条件,CSS 会检查Selector
所选中的元素是否满足这个条件。这个条件通常是关于Selector
的后代元素、子元素、紧邻的同级元素(后一个)或后续的同级元素。
如果 Selector
所选中的元素内部(或紧跟其后)包含了匹配 Relative_Selector
的元素,那么 Selector
这个元素本身就会被选中,然后应用 {}
中的样式。
1.2 它是如何工作的?关键在于“相对选择器”
理解 :has()
的核心在于理解它括号内的“相对选择器”是如何评估的。相对选择器是相对于 :has()
前面的那个 Selector
来进行匹配的。
想象一下:CSS 引擎找到一个潜在的 Selector
元素,然后它会问:“这个元素是否内部包含或者紧跟着一个符合 Relative_Selector
条件的元素?”如果答案是“是”,那么这个 Selector
元素就被选中了。
这里的“包含”不仅仅是直接子元素,可以是任意层级的后代元素。而“紧跟着”则涉及到同级组合器 (+
和 ~
)。
让我们看几个简单的例子来建立直观认识:
示例 1:选择包含 span
的 div
“`html
这是一个段落。
这是一个 span。
这是另一个段落。
“`
css
.container:has(span) {
border: 2px solid blue;
}
解释:
:has(span)
前面的Selector
是.container
。- CSS 找到第一个
.container
元素。 - 它检查这个
.container
内部是否包含span
元素。是的,它有一个<span>这是一个 span。</span>
。 - 所以,第一个
.container
元素被选中,并应用border: 2px solid blue;
样式。 - CSS 找到第二个
.container
元素。 - 它检查这个
.container
内部是否包含span
元素。没有。 - 所以,第二个
.container
元素不被选中。
结果:只有第一个 div.container
会有蓝色边框。
示例 2:选择紧跟着 p
元素的 h2
“`html
标题一
这是段落一。
标题二
“`
css
h2:has(+ p) {
color: green;
}
解释:
:has(+ p)
前面的Selector
是h2
。:has()
括号内的相对选择器是+ p
。这里的+
是一个紧邻兄弟选择器。它表示紧跟在某个元素后面的第一个p
元素。- CSS 找到第一个
h2
元素。 - 它检查这个
h2
后面紧跟着的元素是否是p
。是的,紧跟着的是<p>这是段落一。</p>
。 - 所以,第一个
h2
元素被选中,并应用color: green;
样式。 - CSS 找到第二个
h2
元素。 - 它检查这个
h2
后面紧跟着的元素是否是p
。不是,紧跟着的是<div>...</div>
。 - 所以,第二个
h2
元素不被选中。
结果:只有第一个 h2
会变成绿色。
请注意,h2:has(+ p)
选择的是 h2
元素,而不是 p
元素。这一点非常关键,:has()
总是选择它前面的那个 Selector
。
1.3 :has() 与传统选择器的区别
理解 :has()
的强大之处,需要对比一下它与现有选择器的不同:
- 后代选择器 (
空格):
div span
选择的是div
内部的span
。:has()
(div:has(span)
) 选择的是包含span
的div
本身。它们选择的目标不同。 - 子选择器 (
>
):div > span
选择作为div
直接子元素的span
。:has()
(div:has(> span)
) 选择的是包含一个直接子元素span
的div
本身。同样是选择目标不同,但:has()
括号内可以使用子选择器来指定更精确的匹配条件。 - 紧邻兄弟选择器 (
+
):h2 + p
选择紧跟在h2
后面的p
。:has()
(h2:has(+ p)
) 选择的是前面紧跟着一个h2
的p
… 不对! 等等,仔细想想:has()
的定义:h2:has(+ p)
是选择那个h2
,条件是它后面紧跟着一个p
。所以,h2:has(+ p)
和h2 + p
是完全不同的!前者选择h2
,后者选择p
。 - 通用兄弟选择器 (
~
):h2 ~ p
选择所有在h2
后面的p
。:has()
(h2:has(~ p)
) 选择那个h2
,条件是在它后面有任何一个p
兄弟元素。 - 伪类/伪元素:
:hover
,::before
等通常是基于元素自身状态或位置,:has()
则基于元素内部或紧邻元素的状态/存在。
最关键的区别在于,:has()
使得 CSS 能够进行“逆向”查找或基于内部/后续条件的“父/前序选择”。
第二部分::has() 的强大能力与实际应用场景
了解了基础后,我们来看看 :has()
如何解决实际问题,并让 CSS 变得更灵活和强大。
2.1 选择父元素或祖先元素
这是 :has()
最常被提及的用法,因为它弥补了传统 CSS 的空白。
场景 1:样式化带有特定内容的卡片
假设你有一组卡片 (.card
),有些包含图片 (<img>
),有些不包含。你想让包含图片的卡片有一个不同的背景色。
“`html
标题
一些文本。
另一个标题
更多文本。
“`
“`css
.card {
border: 1px solid #ccc;
padding: 15px;
margin: 10px;
background-color: #fff; / 默认背景 /
}
/ 使用 :has() 选择包含 img 的卡片 /
.card:has(img) {
background-color: #f0f8ff; / 包含图片的卡片背景 /
border-color: blue;
}
“`
解释: :has(img)
会检查每个 .card
元素内部是否有 <img>
标签。第一个 .card
内部有 <img>
,所以它被选中并应用背景色和边框颜色。第二个 .card
没有 <img>
,保持默认样式。
这比使用 JavaScript 检查 .card
内部是否有 img
然后添加一个类 (.card--has-image
) 要简洁和纯粹得多。
你可以进一步精确:
:card:has(> img)
: 只选择直接子元素是img
的卡片。:card:has(.icon)
: 选择包含类名为icon
的元素的卡片。:card:has(p:last-child)
: 选择最后一个子元素是p
的卡片。
场景 2:根据表单输入状态样式化相关的 Label
当一个表单输入框(比如 input
)变成无效状态 (:invalid
) 时,你可能想把与其关联的 label
变成红色,以便提示用户错误。通常 label
在 input
之前。
“`html
“`
“`css
/ 默认 label 样式 /
label {
color: #333;
margin-bottom: 5px;
display: block;
}
/ 使用 :has() 选择后面紧跟着 :invalid input 的 label /
/ 注意:这里不是选择 label 的父元素,而是通过父元素间接关联 /
/ 假设结构是 label 和 input 是同级 /
/ 为了选择 label,我们需要选择它们的共同父元素,再从父元素中选择 label /
/ 如果结构是 fieldset 包裹 label 和 input /
fieldset {
margin-bottom: 15px;
border: none;
padding: 0;
}
/ 选择包含 :invalid input 的 fieldset,然后选中该 fieldset 内的 label /
fieldset:has(:invalid) label {
color: red;
}
/ 或者更直接一点,如果 label 紧跟着 input,可以使用通用兄弟选择器来选择父元素 /
/* 假设结构是:
*/
div:has(input:invalid) label {
color: red;
}
/ 如果 label 就在 input 前面作为紧邻兄弟,并且你想选的是 label 自身,
这是 :has() 目前做不到的,因为它选的是 :has() 前面的元素。
你不能写 label:has(- input:invalid) 因为没有负向的兄弟选择器。
但你可以选共同的父元素。
/
“`
解释: 在包含 label
和 input
的父容器(例如 fieldset
或 div
)上使用 :has(input:invalid)
。如果这个父容器内部有一个状态为 :invalid
的 input
,那么这个父容器就会被选中。然后,我们利用这个被选中的父容器,通过后代选择器选中它内部的 label
并应用样式。这优雅地解决了“根据后代状态影响前序同级元素”的问题。
2.2 基于兄弟元素条件的选择
虽然 :has()
允许在括号内使用兄弟选择器 (+
, ~
),但如前所述,这仍然是用来筛选 :has()
前面的那个元素。它不能让你直接选择紧跟着一个特定元素的前一个兄弟元素(CSS 仍然没有“前一个兄弟选择器”)。
但是,:has()
可以用来选择前一个兄弟元素所包含特定内容的当前元素。或者,更常见的是,选择一个元素,如果它后面紧跟着或有某个兄弟元素。
场景 3:样式化列表中的最后一个不含特定类的项
假设一个列表 (ul
),有些列表项 (li
) 有一个 .special
类。你想给最后一个(在 DOM 顺序上)不包含 .special
类的 li
加上特殊的下边框。
“`html
- Item 1
- Item 2 (Special)
- Item 3
- Item 4
- Item 5 (Special)
- Item 6
“`
在过去,这很难做到。你不能直接写 :not(.special):last-child
,因为 Item 6
是 last-child
,但它没有 .special
类,不符合条件;Item 4
也没有 .special
类,但它不是 last-child
。你需要找到最后一个 :not(.special)
的 li
。
使用 :has()
:
“`css
li:not(.special):has(~ li:last-child) {
/ This selects any li that is not .special AND has the last-child li after it. /
/ This doesn’t quite work for selecting the last non-special li /
/ Let’s rethink: We want the li that is NOT special, AND is the LAST li that is NOT special. /
/ How to express “is the last of its kind”? /
/ An element is the “last of its kind” if there are no more elements of that kind AFTER it. /
/ Select li that is not .special /
/ AND has no following sibling li that is also not .special /
}
li:not(.special):has(~ li:not(.special)) {
/ This selects any li that is not .special AND is followed by another li that is not .special /
/ This helps us select all except the last non-special one /
}
li:not(.special):not(:has(~ li:not(.special))) {
/ This selects any li that is not .special /
/ AND does NOT have any following sibling li that is also not .special /
/ This is it! It selects the last li that does not have the .special class /
border-bottom: 3px dashed orange;
}
“`
解释:
li:not(.special)
: 选择所有没有.special
类的li
元素。:has(~ li:not(.special))
: 检查前面选中的li
后面是否还有任何没有.special
类的li
兄弟元素 (~
表示后面的所有兄弟元素)。li:not(.special):has(~ li:not(.special))
: 组合起来,选择所有没有.special
类,并且后面还有其他没有.special
类的li
的元素。换句话说,它选中了除了最后一个没有.special
类的li
之外的所有符合条件的li
。:not(:has(~ li:not(.special)))
: 取反,表示那个没有.special
类,并且后面没有其他没有.special
类的li
兄弟元素的元素。这正是我们想要的最后一个没有.special
类的li
。
结果:Item 4
会有橙色虚线边框。
这个例子有点绕,但展示了 :has()
结合 :not()
和兄弟选择器解决复杂同级元素选择问题的能力。
2.3 结合伪类和伪元素
:has()
括号内不仅可以使用元素选择器、类、ID 等,还可以结合其他伪类和伪元素(虽然对伪元素的支持有限,通常更侧重结构和状态伪类)。
场景 4:样式化包含必填输入框的表单
你想给包含至少一个 required
属性的输入框的表单 (form
) 加上一个特殊的顶部边框。
“`html
“`
css
form:has(:required) {
border-top: 5px solid orange;
padding-top: 10px;
}
解释: 第一个 form
内部包含一个具有 :required
伪类状态的 input
。因此,第一个 form
被选中,并应用样式。第二个 form
内部没有 required
字段,所以不被选中。
这比给所有包含必填项的表单手动添加一个类(比如 .form--has-required
)要方便得多。
场景 5:基于列表中项的数量进行样式化
你可能想给列表项数量不同的列表应用不同的样式。例如,如果一个 ul
只有一项,让它居中;如果有三项或更多,改变其布局。
“`html
- Only one item
- Item A
- Item B
- Item X
- Item Y
- Item Z
- Item W
“`
“`css
/ 列表只有一项时 /
ul:only-child:has(li:only-child), / 如果 ul 是父元素的唯一子元素,并且其 li 也是其父 ul 的唯一子元素 (冗余,但说明可以组合) /
ul:has(li:only-child) / 更简洁:选择包含唯一子元素 li 的 ul /
{
list-style: none;
padding: 0;
text-align: center; / 让唯一的 li 居中 /
}
/ 列表有三项或更多时 /
ul:has(li:nth-child(3)) {
/ 选择包含至少第三个 li 的 ul /
/ nth-child(3) 存在意味着至少有 3 个子元素 /
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
padding: 0;
}
“`
解释:
ul:has(li:only-child)
选择内部唯一的子元素是li
的ul
。第一个ul
符合条件。ul:has(li:nth-child(3))
选择内部包含至少第三个li
子元素的ul
。第三个ul
包含四个li
,所以第三个li
存在,该ul
被选中。
这比通过 JavaScript 计算 ul
的子元素数量然后添加类更具声明性。
2.4 复杂的选择器组合
:has()
的括号内可以使用任意复杂的组合选择器(除了 :has()
自身,防止无限循环,尽管规范允许有限的嵌套)。
场景 6:样式化包含特定结构的文章
你想给一个 <article>
元素加上特殊的样式,如果它包含一个紧跟在 h2
后面的 figure
元素,并且这个 figure
内部有一个带有 figcaption
的 img
。
“`html
章节标题
一些内容。

更多内容。
另一个章节
内容…
内容…
小标题
“`
css
article:has(h2 + figure:has(img:has(+ figcaption))) {
border: 2px dashed purple;
background-color: #f9f0ff;
}
解释:
article:has(...)
: 我们要选择article
。h2 + figure
: 在article
内部,查找一个紧跟在h2
后面的figure
。figure:has(...)
: 这个找到的figure
,还要满足一个条件。img:has(+ figcaption)
: 在figure
内部,查找一个img
,并且这个img
后面紧跟着一个figcaption
兄弟元素。
只有第一个 <article>
同时满足所有条件:它包含 h2
,h2
后面紧跟着 figure
,而这个 figure
内部有 img
并且 img
后面紧跟着 figcaption
。所以第一个 <article>
被选中。
这个例子虽然复杂,但展示了 :has()
可以嵌套使用,并且括号内可以包含复杂的选择器链,这提供了极高的灵活性。
2.5 提高代码可读性和可维护性
使用 :has()
可以在许多情况下减少 HTML 中的类名或使用 JavaScript 进行 DOM 操作。这使得 HTML 结构更纯净,CSS 更直接地反映了基于结构的样式意图,提高了代码的可读性和可维护性。
例如,之前给包含图片的卡片添加类:
“`html
“`
现在可以只写:
“`html
“`
CSS 处理逻辑:
css
.card:has(img) { ... }
这使得 HTML 更多地关注内容和结构,而将样式逻辑放在 CSS 中,是更好的关注点分离。
第三部分:浏览器支持与注意事项
3.1 浏览器支持
:has()
是一个相对较新的特性。在撰写本文时(2023 年末至 2024 年初),主流现代浏览器都已经提供了良好的支持:
- Chrome:105+ 支持
- Firefox:105+ 支持
- Safari:15.4+ 支持
- Edge:105+ 支持
你可以访问 caniuse.com 查看最新的、详细的浏览器支持情况。
对于需要兼容旧版浏览器的项目,:has()
可能不是首选,或者需要提供回退方案。
3.2 性能考虑
:has()
需要浏览器在匹配 :has()
前面的元素后,进一步检查其后代或兄弟元素。这可能会比简单的类或标签选择器消耗更多性能。然而,现代浏览器引擎在处理 :has()
方面已经进行了大量的优化。
在大多数常见用例中(例如,样式化直接父元素基于其子元素),:has()
的性能开销通常可以忽略不计。
但如果你的 :has()
选择器非常复杂,涉及到深层嵌套或大量的兄弟元素,并且应用在 DOM 结构非常庞大或样式频繁变化的场景(例如,复杂的动态列表),理论上可能会有轻微的性能影响。
最佳实践是:
- 避免在高性能敏感的地方使用过于复杂的
:has()
。 - 优先使用更直接的
:has()
形式(例如:has(> .child)
而不是:has(.grandchild .deep-nested-element)
)。 - 进行性能测试,尤其是在遇到性能瓶颈时。
在绝大多数日常开发场景中,:has()
的便利性远大于其潜在的轻微性能开销。
3.3 特殊限制
- 不能在
:has()
括号内使用伪元素: 你不能写div:has(::before)
或div:has(p::first-letter)
。:has()
括号内的选择器目前主要用于匹配实际存在的元素和它们的状态(伪类)。 - 不能选择
:has()
括号内的元素: 再次强调,div:has(span)
选择的是div
,不是span
。你不能用div:has(span)
这个规则块来直接给span
样式。如果你想给span
样式,你需要写一个新的规则,比如div:has(span) span { ... }
。 - 避免无限循环: 规范限制了
:has()
的某些嵌套方式,以防止无限递归。例如,你不能写div:has(:has(span))
(尽管某些更复杂的嵌套可能被允许),也不能写div:has(&)
(如果&
引用自身的话)。
3.4 特定性(Specificity)
:has()
自身的特定性是 0,0,0
。但是,它所包含的最具特定性的选择器的特定性会加到 :has()
前面那个选择器的特定性上。
例如:
div:has(span)
: 特定性是div
的特定性 +span
的特定性。 (0,0,1 + 0,0,1 = 0,0,2).card:has(img)
: 特定性是.card
的特定性 +img
的特定性。 (0,1,0 + 0,0,1 = 0,1,1)#main:has(.sidebar > p)
: 特定性是#main
的特定性 +.sidebar > p
中最 specific 的.sidebar
的特定性。 (1,0,0 + 0,1,0 = 1,1,0)
理解 :has()
的特定性规则很重要,因为它会影响样式覆盖的优先级。
第四部分:总结与展望
CSS :has()
伪类选择器是现代 CSS 中最重要的新增特性之一。它通过允许我们基于后代、子元素或后续兄弟元素的状态和存在来选择父元素、祖先元素或前序兄弟元素(通过选择共同祖先),极大地增强了 CSS 的选择能力。
通过 :has()
,我们可以:
- 实现“父选择器”和“前一个兄弟选择器”的功能(通过选择共同父元素)。
- 基于元素的内部结构或内容应用样式,而无需修改 HTML 或使用 JavaScript。
- 编写更具声明性、更简洁的 CSS 代码。
- 减少对辅助类名的依赖,使 HTML 结构更纯净。
- 解决许多之前用纯 CSS 难以或无法解决的样式问题。
尽管需要注意浏览器兼容性和在极端情况下的性能问题,但在现代 Web 开发中,:has()
已经成为一个非常有用的工具,能够帮助你构建更灵活、更易于维护的界面。
如果你是 CSS 初学者,掌握 :has()
将让你站在更高的起点。如果你是有经验的开发者,:has()
将为你打开新的思路,解决曾经棘手的问题。
现在,是时候在你的项目中尝试使用 :has()
了!从简单的父子关系开始,逐渐探索它在处理同级元素、表单状态、列表结构等场景下的强大魔力。通过实践,你将更深刻地理解它如何革新你的 CSS 编写方式。
希望这篇详细的入门教程对你有所帮助!祝你学习愉快,写出更优雅的 CSS 代码!