CSS :has() 交互指南


原文 CSS :has() Interactive Guide

1. 什么是 :has()?

:has() 常被称为 “父级选择器”。它允许你根据一个元素内部是否包含某些子元素(或符合某种条件的后代元素)来给该元素本身设置样式。 本文将会探讨此问题,并介绍 :has() 的一些常见用法。

1.1 痛点

假设我们有一个 <figure> 标签,当它包含 <figcaption> 标签时,我们希望给 <figure> 添加一个样式。我们应该如何实现呢?

1
2
3
4
<figure>
<img src="thumb.jpg" alt="" />
<figcaption>A great looking tart.</figcaption>
</figure>

当有说明文字时,我希望图片显示以下效果:

  • Paddding
  • Background
  • Shadow

在 CSS 中,唯一可行的方法是给 <figure> 标签手动添加一个类名(class),然后通过该类名来选择其中的 <figcaption>

1
2
3
4
5
6
figure.with-caption {
padding: 0.5rem;
background-color: #fff;
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.1);
border-radius: 8px;
}

如果 HTML 是由后台编辑器或 Markdown 引擎动态生成的,你很难手动给包含标题的图片(figure)精准地加上 .with-caption 这个类。

1.2 解决办法

有了 CSS :has() 选择器,就好办多了。你可以像下面这样写 CSS:

1
2
3
4
5
figure:has(figcaption) {
padding: 0.5rem;
background-color: #fff;
border-radius: 8px;
}
1
2
3
4
<figure>
<img src="thumb.jpg" alt="" />
<figcaption>A great looking tart.</figcaption>
</figure>

而这仅仅是使用 CSS :has() 的冰山一角。

复习 CSS 选择器

在我们深入之前,先来复习一下 CSS 选择器。

2.1 相邻兄弟选择器

要选择元素的下一个同级元素,我们可以使用相邻兄弟选择器 (+)。用于选择紧接在另一个元素后的元素,且二者有相同的父元素。

1
2
3
4
5
6
7
.book {
opacity: 0.2;
}

.frame + .book {
opacity: 1;
}

2.2 通用兄弟选择器

要选择元素的下一个同级元素,我们可以使用通用兄弟选择器 (~)。用于选择紧接在另一个元素后的元素,且二者有相同的父元素。

1
2
3
4
5
6
7
.book {
opacity: 0.2;
}

.frame ~ .book {
opacity: 1;
}

2.3 前一个兄弟选择器

借由 :has(),我们现在可以实现选择 “后面紧跟 B 的 A 元素”,从而间接实现了前一个兄弟选择器的功能。

以下代码实现如下功能:选中一个 .book 元素,前提是它的紧后方(+)紧跟着一个 .frame 元素。

1
2
3
4
5
6
7
.book {
opacity: 0.2;
}

.book:has(+ .frame) {
opacity: 1;
}

基于上述,我们可以选择某个特定元素前的所有元素。以下代码实现选择 .frame 元素之前所有 .books 元素。

1
2
3
4
5
6
7
.book {
opacity: 0.2;
}

.book:has(~ .frame) {
opacity: 1;
}

2.5 :not 伪类

:not() 伪类选择器在排除某个元素时非常有用。比如,当我们想要选择所有不包含 .blue 的 .book 元素时,可以这样写:

1
2
3
.book:not(.blue) {
opacity: 1;
}

3. CSS :has () 选择器匹配

学会阅读 CSS :has() 选择器非常有用。在本节中,我将通过几个示例向你展示如何分析它们。

以下是一些供你练习的通用示例。

3.1 带有图片的卡片

在这个例子中,我们有一个包含图片子元素的卡片。我们可以通过 CSS :has() 来检查这一点。

选择所有包含 img.card

1
.card: has(img);
1
2
3
4
<div class="card">
<img src="thumb.jpg" alt="" />
<div class="card-content"></div>
</div>

如果我们想要一个仅在图片是卡片的直接子元素时才匹配的选择器呢?

选择所有包含直接 img 子元素的 .card

1
.card: has(> img);

存在 .card-thumb 包裹层,它将不会匹配。

1
2
3
4
5
6
<div class="card">
<div class="card-thumb">
<img src="thumb.jpg" alt="" />
</div>
<div class="card-content"></div>
</div>

3.2 不带图片的卡片

CSS :has() 可以与 :not() 选择器结合使用。在这种情况下,只有当卡片不包含图片时,选择器才会匹配。

选择所有不包含 img.card

1
.card: not(: has(img));
1
2
3
<div class="card">
<div class="card-content"></div>
</div>

3.3 相邻兄弟选择器与 :has

假设我们要选择包含“相框后紧跟紫色书本”的层架(.shelf)。可以这样写:

选择包含“相框 + 紫色书本”组合的层架:

1
.shelf: has(.frame + .book-purple);

3.4 选中仅包含盒子的层架

在这个例子中,CSS :has() 选择器尝试匹配盒子容器内部仅包含书本的情况。

选择拥有书本容器的层架:

1
.shelf: has(.box > .book);

3.5 选择没有蓝色书本的盒子

:has():not() 结合是 CSS :has() 的多种玩法之一。在这个例子中,只有当盒子内没有蓝色书本时,选择器才会匹配。

选择不包含蓝色书本的盒子:

1
.box: not(: has(.blue));

3.6 选择有 3 本以上书本的盒子

选择包含 3 个或更多书本的 .box

1
.box: has(.book: nth-last-child(n + 3));

3.7 根据数量更改书本排列方向

一种有趣的方法是根据书本的数量,将书本从“堆叠”状态改为“立起”状态。如果书本数量达到 5 本或更多,它们将变为立起状态 这可以通过使用 :nth-last-child()实现:

1
2
3
4
5
6
7
.shelf:has(.book:nth-last-child(n + 5)) {
flex-direction: row;
.book {
height: 100%;
width: 22px;
}
}

3.8 当有 5 本以上书本时,每隔 3 本添加间距

这是上一个示例的延伸。如果书本数量为 5 本以上,我们需要每隔 3 本添加一个间距,以便更好地整理层架。

1
2
3
4
5
.shelf:has(.book:nth-last-child(n + 5)) {
.book:nth-child(3n) {
margin-right: 1rem;
}
}

3.9 书架综合示例

这是一个有趣的例子,我用 CSS :has() 设置了以下条件:

  1. 根据数量将书本从堆叠改为立起。
  2. 如果书本达到一定数量,将相框挂在墙上。
  3. 如果实在没地方了,把植物和地球仪扔到地上。

3.9.1 改变书本显示

我首先要做的是,当书本数量超过 5 本时,将书本从堆叠改为立起。

1
2
3
4
5
6
7
.shelf:has(.book:nth-last-child(n + 5)) {
flex-direction: row;
.book {
height: 100%;
width: 22px;
}
}

3.9.2 将相框挂在墙上

1
2
3
4
5
6
7
.shelf:has(.book:nth-last-of-type(n + 6)) {
.frame {
position: absolute;
left: calc(50% - (75px / 2));
top: -165%;
}
}

3.9.3 将植物和地球仪扔到地上

当书本达到 9 本以上时(完全没空间了),触发植物和地球仪掉落的动画:

1
2
3
4
5
6
7
8
.shelf:has(.book:nth-last-of-type(n + 9)) {
.plant {
animation: movePlant 0.6s forwards;
}
.earth {
animation: moveEarth 0.6s forwards;
}
}

3.10 CSS :has() 中的逻辑运算符

借助 CSS :has(),我们可以模拟类似于编程语言中的逻辑运算符,如 “&&”(与)和 “||”(或)。在下面的演示中,我们有以下条件:

  1. 如果层架(shelf)同时包含紫色书黄色书,则选中该层架。
  2. 如果层架包含紫色书黄色书,则选中该层架。

你可以尝试以下操作:

  • 切换书本的显示/隐藏。
  • 在菜单中切换“and”(与)/ “or”(或)选项。

在 CSS 中,实现逻辑如下:

OR(或)运算符

:has() 的括号内使用逗号分隔多个选择器,只要满足其中任意一个条件,父元素就会被选中。

1
2
3
4
/* 只要包含紫色书 或 黄色书 */
.shelf:has(.bookPurple, .bookYellow) {
outline: dashed 2px deeppink;
}

AND(与)运算符

通过连写多个 :has() 伪类,只有当所有条件都同时满足时,父元素才会被选中。

1
2
3
4
/* 必须同时包含紫色书 和 黄色书 */
.shelf:has(.bookPurple):has(.bookYellow) {
outline: dashed 2px deeppink;
}

能够在 CSS :has() 中使用逻辑运算符,这简直太神奇了。

希望你现在已经掌握了 CSS :has() 的工作原理。接下来,让我们进入一些更有趣的 CSS :has() 实际应用案例。