CSS :has()入门教程 – wiki基地


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:选择包含 spandiv

“`html

这是一个段落。

这是一个 span。

这是另一个段落。

“`

css
.container:has(span) {
border: 2px solid blue;
}

解释:

  1. :has(span) 前面的 Selector.container
  2. CSS 找到第一个 .container 元素。
  3. 它检查这个 .container 内部是否包含 span 元素。是的,它有一个 <span>这是一个 span。</span>
  4. 所以,第一个 .container 元素被选中,并应用 border: 2px solid blue; 样式。
  5. CSS 找到第二个 .container 元素。
  6. 它检查这个 .container 内部是否包含 span 元素。没有。
  7. 所以,第二个 .container 元素不被选中。

结果:只有第一个 div.container 会有蓝色边框。

示例 2:选择紧跟着 p 元素的 h2

“`html

标题一

这是段落一。

标题二

这是一个 div,不是 p。

“`

css
h2:has(+ p) {
color: green;
}

解释:

  1. :has(+ p) 前面的 Selectorh2
  2. :has() 括号内的相对选择器是 + p。这里的 + 是一个紧邻兄弟选择器。它表示紧跟在某个元素后面的第一个 p 元素。
  3. CSS 找到第一个 h2 元素。
  4. 它检查这个 h2 后面紧跟着的元素是否是 p。是的,紧跟着的是 <p>这是段落一。</p>
  5. 所以,第一个 h2 元素被选中,并应用 color: green; 样式。
  6. CSS 找到第二个 h2 元素。
  7. 它检查这个 h2 后面紧跟着的元素是否是 p。不是,紧跟着的是 <div>...</div>
  8. 所以,第二个 h2 元素不被选中。

结果:只有第一个 h2 会变成绿色。

请注意,h2:has(+ p) 选择的是 h2 元素,而不是 p 元素。这一点非常关键,:has() 总是选择它前面的那个 Selector

1.3 :has() 与传统选择器的区别

理解 :has() 的强大之处,需要对比一下它与现有选择器的不同:

  • 后代选择器 ( 空格): div span 选择的是 div 内部的 span:has() (div:has(span)) 选择的是包含 spandiv 本身。它们选择的目标不同。
  • 子选择器 (>): div > span 选择作为 div 直接子元素的 span:has() (div:has(> span)) 选择的是包含一个直接子元素 spandiv 本身。同样是选择目标不同,但 :has() 括号内可以使用子选择器来指定更精确的匹配条件。
  • 紧邻兄弟选择器 (+): h2 + p 选择紧跟在 h2 后面的 p:has() (h2:has(+ p)) 选择的是前面紧跟着一个 h2p不对! 等等,仔细想想 :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 变成红色,以便提示用户错误。通常 labelinput 之前。

“`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) 因为没有负向的兄弟选择器。
但你可以选共同的父元素。
/
“`

解释: 在包含 labelinput 的父容器(例如 fieldsetdiv)上使用 :has(input:invalid)。如果这个父容器内部有一个状态为 :invalidinput,那么这个父容器就会被选中。然后,我们利用这个被选中的父容器,通过后代选择器选中它内部的 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 6last-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;
}
“`

解释:

  1. li:not(.special): 选择所有没有 .special 类的 li 元素。
  2. :has(~ li:not(.special)): 检查前面选中的 li 后面是否还有任何没有 .special 类的 li 兄弟元素 (~ 表示后面的所有兄弟元素)。
  3. li:not(.special):has(~ li:not(.special)): 组合起来,选择所有没有 .special 类,并且后面还有其他没有 .special 类的 li 的元素。换句话说,它选中了除了最后一个没有 .special 类的 li 之外的所有符合条件的 li
  4. :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) 选择内部唯一的子元素是 liul。第一个 ul 符合条件。
  • ul:has(li:nth-child(3)) 选择内部包含至少第三个 li 子元素的 ul。第三个 ul 包含四个 li,所以第三个 li 存在,该 ul 被选中。

这比通过 JavaScript 计算 ul 的子元素数量然后添加类更具声明性。

2.4 复杂的选择器组合

:has() 的括号内可以使用任意复杂的组合选择器(除了 :has() 自身,防止无限循环,尽管规范允许有限的嵌套)。

场景 6:样式化包含特定结构的文章

你想给一个 <article> 元素加上特殊的样式,如果它包含一个紧跟在 h2 后面的 figure 元素,并且这个 figure 内部有一个带有 figcaptionimg

“`html

章节标题

一些内容。

图表
图表说明。

更多内容。

另一个章节

内容…

图片

内容…

小标题

“`

css
article:has(h2 + figure:has(img:has(+ figcaption))) {
border: 2px dashed purple;
background-color: #f9f0ff;
}

解释:

  1. article:has(...): 我们要选择 article
  2. h2 + figure: 在 article 内部,查找一个紧跟在 h2 后面的 figure
  3. figure:has(...): 这个找到的 figure,还要满足一个条件。
  4. img:has(+ figcaption): 在 figure 内部,查找一个 img,并且这个 img 后面紧跟着一个 figcaption 兄弟元素。

只有第一个 <article> 同时满足所有条件:它包含 h2h2 后面紧跟着 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 代码!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部