CSS 嵌套

通过浏览器原生支持的 CSS 嵌套,可以大幅减少编写的CSS代码量,同时使得代码更易于维护和理解。

CSS嵌套是CSS的一种新语法,允许将选择器嵌套在其他选择器内,其中每个嵌套的选择器都相对于其父级。

它的最简单形式如下:

1
2
3
4
5
6
7
8
9
.card {
border: 2px solid red;
padding: 1rem;

.card-title {
font-family: Jolly;
font-size: 3rem;
}
}

浏览器会将应用于具有类名为card-title的元素的样式仅限于该元素嵌套在具有类名为card的元素内。换句话说,浏览器会按照以下方式翻译:

1
2
3
4
5
6
7
8
9
.card {
border: 2px solid red;
padding: 1rem;
}

.card .card-title {
font-family: Jolly;
font-size: 3rem;
}

原生CSS嵌套的好处

减少输入

在上述“翻译”版本中,您可以看到浏览器为我们复制了.card,但在我们的源代码中,我们只需要写一次。通过嵌套选择器,如果要防止样式应用得太广泛,您就不再需要重复选择器的一部分。此外,减少输入还意味着您的文件最终会更小,因此下载速度更快。

更容易进行范围限定

这也意味着更容易进行范围限定。如果您正在处理卡片,您只需写一次.card选择器,并在其中嵌套所有相关的CSS。这意味着您需要编写较少特定的选择器,因为它们已经作用于其父元素。这还使您的代码更具可移植性,因为您不必浏览CSS以找到应用于卡片的所有样式,而可以直接使用.card和其中的所有内容。

CSS遵循DOM结构

这样做的好处是,您的CSS开始模仿您的DOM。在上面的例子中,card-titleCSS中和在DOM中都是嵌套的。当您的CSS模仿您的DOM时,更容易理解和理解更改的效果。如果您曾经在大型CSS代码库上工作过,您会知道即使您认为不再使用CSS,删除CSS也是多么危险。

当您的CSS遵循HTML结构时,您可以更加自信地删除对象。例如,当您从卡片HTML中删除card-footer时,您知道您的.card-footer CSS只能应用于站点的该部分,因此您也可以从CSS中将其删除。

嵌套的复杂性

现在您了解了CSS嵌套的有用之处,让我们来探讨一些复杂性。

浏览器支持

此刻我们需要指出,目前有两个版本的CSS嵌套:严格版本,即Chromium版本到119和Safari版本到17.1实现的版本,以及“宽松”版本,即Firefox随附的版本,并且现在在Safari 17.2Chromium 120中也可用。

它们之间的区别在于它们如何处理嵌套的元素选择器(如p、div)。严格版本允许任何以非字母开头的选择器在没有“嵌套选择器”&的情况下嵌套。这意味着不需要这样:

1
2
3
4
5
6
7
8
9
.card {
border: 2px solid red;
padding: 1rem;

h3 {
font-family: Jolly;
font-size: 3rem;
}
}

我们不得不写成下面这样

1
2
3
4
5
6
7
8
9
.card {
border: 2px solid red;
padding: 1rem;

& h3 {
font-family: Jolly;
font-size: 3rem;
}
}

否则,您的h3样式将不会被应用。其原因在于属性(例如margin)也以字母开头,因此浏览器的CSS引擎无法确定您是在添加新属性还是新的嵌套选择器。

浏览器找到了一种解决方法,因此宽松版本允许您在没有“&”的情况下嵌套元素选择器,使一切变得更加简单。到您阅读本文时,Chromium 120和Safari 17.2将已发布,因此所有浏览器现在都支持宽松版本。

建议您继续使用严格版本,因为这将在所有浏览器上保持可用。

嵌套选择器及浏览器解析嵌套的方式

那么,这是否意味着您不再需要嵌套选择器呢?对于简单的嵌套来说,不再需要。但是嵌套选择器使您能够控制嵌套的方式。为了理解它是如何实现的,您需要了解三件事:

  • 如果嵌套选择器没有“&”,浏览器会在其前面(带有空格)隐式地添加一个
  • CSS引擎中,“&”被替换为“is(<parent selector>)”
  • 您可以在选择器的任何位置添加“&”(包括多次)

让我们按顺序详细了解这些事项。

隐式嵌套选择器

回到我们的第一个例子,嵌套的.card-title没有嵌套选择器,因此浏览器会隐式地添加一个:

1
2
3
4
5
6
7
8
9
.card {
border: 2px solid red;
padding: 1rem;

(implicit '&') .card-title {
font-family: Jolly;
font-size: 3rem;
}
}

单独来看,这没什么大不了的,但在了解到“&”会被替换时,这一点很重要。

解析的选择器

“&”被替换为“is(<parent selector>)”,因此上述代码中的嵌套选择器最终不是如本文开头所说的 .card .card-title,而是 :is(.card) .card-title

在这个例子中,这没什么大不了的,因为它们都具有相同的权重(0,2,0),但如果您的父选择器是以逗号分隔的选择器,您可能会意外地制定一个特异性非常高的权重:

1
2
3
4
.card,
#xmas-card {
.card-title { ... }
}

这将被解析为 :is(.card, #xmas-card) .card-title,其权重为 (1,1,0),因为:is()伪选择器将获得最特定项的权重,即在这种情况下是 id。如果您的 .card-title 完全不在 #xmas-card 中,而是在常规的 .card 中,情况也是如此。

如果您意识到 :is() 选择器也会被嵌套,情况可能变得更加复杂。让我们看看在以下三层深度嵌套中是如何工作的:

1
2
3
4
5
.card {
.card-body {
p { ... }
}
}

首先,“p”是隐式的 & p。这将替换为父级,为了方便起见,我们也会添加 &,创建 :is(& .card-body) p。最后,我们还在那里填入 "&",用:is(.card)替换它,结果是 :is(:is(.card) .card-body) p,这是您的浏览器最终使用的内容。

添加 & 选择器

您可以通过添加 & 选择器来明确嵌套方式。我个人认为这样更易读,而且除此之外,它还会影响嵌套的工作方式。这是因为空格也是一种选择器:后代选择器。因此,您是否在 & 后面添加空格可能会有所不同。

以下代码将在悬停卡片时应用:

1
2
3
.card {
&:hover { ... }
}

这是因为它解析为 :is(.card):hover

以下代码将产生不同的效果:

1
2
3
.card {
& :hover { ... }
}

由于存在空格,:hover 部分现在是在卡片的子节点上生效,因此它的作用类似于 :is(.card) *:hover。伪类如:hover在它们之前会有一个隐式的 *(通用元素选择器)。所以请记住这个空格。

通过在选择器的其他地方添加显式的 & 选择器,你可以针对嵌套的不同部分进行定位。例如,要为卡片添加样式,但仅当它们在列表中时,可以这样做:

1
2
3
.list {
.card { ... }
}

不利之处是你的卡片组件的所有样式也将具有 .list 作为父选择器,使其更具体且不够模块化。

相反,你可以这样写:

1
2
3
.card {
.list & { ... }
}

这样,所有其他卡片样式都可以被限定在卡片内,当卡片在列表中时的额外样式也会随之包含,保持整体简洁而模块化。

添加多个选择器在你想要按顺序样式化元素时非常有用。例如,在列表中为每个卡片添加边距,除了第一个:

1
2
3
4
5
.card {
.list & + & {
margin-top: 1rem;
}
}

嵌套@规则

CSS嵌套的一个非常有用的部分是,你还可以嵌套诸如@media、@supports等规则。

与其编写以下代码:

1
2
3
4
5
6
7
8
9
.card {
max-width:100%;
}

@media (min-width: 50rem) {
.card {
max-width: 40rem
}
}

你可以嵌入media query

1
2
3
4
5
6
7
.card {
max-width:100%;

@media (min-width: 50rem) {
max-width: 40rem
}
}

注意我们无需重复.card,而且我们可以直接在媒体查询中使用CSS声明,因为浏览器会隐式地将其解读为:

1
2
3
4
5
6
7
.card {
@media (min-width: 50rem) {
& {
max-width: 40rem
}
}
}

而且正如我们之前学到的那样,该&然后被替换为:is(.card)

注意事项

当我第一次使用Sass时,我对嵌套情有独钟,有时嵌套了20层深。在构建样式时,一直进行下去确实很诱人。但是每一层的嵌套也会增加选择器的特异性,使得以后编辑变得更加困难(更不用说缩进了!)。

一个简单的经验法则是,当你达到3或4层深度时,看看是否可以进入新的嵌套集。这样可以使您的选择器更容易理解,并且可能存在组件及其子组件的逻辑分隔点。有多种其他应对此情况的策略,比如保留空的父选择器,以及在布局和样式之间进行拆分。

另一个棘手的部分是,由于嵌套被解析为非嵌套代码,您编写CSS的顺序可能不是CSS应用的顺序。简而言之:您可以在应用常规属性之前嵌套@规则,如下所示:

1
2
3
4
5
6
7
body { 
@media all {
background: red;
}

background: blue;
}

如果按照CSS的顺序进行,背景将是蓝色,因为@规则不会添加特异性,而background: blue最后出现。

一旦浏览器完成计算CSS值,它最终应用规则如下,并将媒体查询移到最后:

1
2
3
4
5
6
7
8
9
body {
background: blue;
}

@media all {
:is(body) {
background: red;
}
}

现在,background: red是最后一个,最终成为应用的背景颜色。

处理这个问题,保持代码易于理解的最佳方式是仅在所有常规属性之后嵌套,即使嵌套语法允许混合它们。

总结

CSS嵌套在所有浏览器中都可用,尽管为了保持最广泛的兼容性,您可能希望继续使用严格的表示法。

嵌套有助于使您的CSS更小,更易于理解,并更紧密地遵循您正在样式化的DOM。在使用嵌套时,请尽量避免嵌套太深,并记住您的CSS需要被解析,这可能会改变浏览器最终使用的顺序和选择器。