层叠图层及其嵌套

本章概要

  • 按合理的逻辑组织 CSS
  • 用图层(layers)来控制层叠行为
  • 伪类 :is():where() 的使用
  • 通过嵌套对相关样式进行分组

在本书第一部分(译注:即第 1 ~ 3 章)和第二部分(译注:即第 4 ~ 7 章)中,我们了解了 CSS 的复杂性,学习了 CSS 在布局页面元素方面提供的各类工具,并先后将盒模型(box model)、外边距折叠(margin collapsing)、堆叠上下文(stacking contexts)、Flexbox 布局以及网格布局等工具收入囊中。这些技能会在您的项目开发中、尤其是在刚启动一个新项目的时候派上用场。然而,在软件开发领域,编程并不仅仅只是为了让代码运行起来,与之同等重要的,是用易于理解的方式来组织代码,以便后续进行更改时代码不会变得脆弱不堪。具体到 CSS 中,这将带来一系列全新的困难与挑战。

在过去,开发人员应对这些问题的唯一方法,就是遵循严格规定的方法论与命名规范;而如今,CSS 提供了全新的功能来打破这一僵局。这些一流的功能设计为代码组织提供了更为理想的解决方案,并成功实现了对层叠(cascade)更为明确的控制。

本书第 3 部分将重点介绍这些功能特性,教会您通过它们来驾驭上述问题的要领,并带您了解 CSS 架构相关的知识。这些章节与其他章节略有不同,论述的重点不再是样式表中的某些具体声明,而是更侧重于所选的选择器,以及优先显示目标样式所涉及的层叠操作。此外,我还将站在更高的层面带您审视样式表的整体结构,使其条理清晰、易于理解。

尽管这些工具适用于任何规模的项目,但它们主要用于应对网站或应用在规模与复杂性日益增长的情况下出现的这些问题。这才是它们真正的用武之地。

由于这些章节中的很多功能都是 CSS 的最新特性,其中部分功能可以直接现学现用,而另一部分的成熟应用可能还尚需时日。撰写本书时,我对未来始终持乐观态度,希望它们都能在不久的将来成为新的惯例。虽然在介绍每一项新功能时,我都会指出当前浏览器的支持情况,但在阅读本书时,请务必查看最新的统计数据。这里推荐两个挺不错的参考资源:https://developer.mozilla.org/https://caniuse.com/

8.1 用 layer 图层来操控层叠规则

Manipulating the cascade with layers

说起 CSS 的层叠规则,浏览器会简单地对样式应用一系列规则,以确保在两个规则集恰巧应用到同一元素时,其中一个规则集能根据既定规则覆盖掉另一个。选择器的优先级与源码顺序是解决样式冲突的一种可靠途径;但有时它们也未必能准确传达开发者在 CSS 优先级上的真实意图。在设计样式表时,有个很常见的场景是希望优先级较低的规则集胜出。例如,大多数样式表都会通过以下 CSS 代码来设置页面链接的基础样式:

a:any-link {
  color: var(--brand-blue);
  font-weight: bold;
}

该链接选择器包含一个标签与一个伪类,其优先级为 0, 1, 1;而在页面的其他位置可能有个下面这样的链接,被设计成了一个大型的引导按钮:

<a class="button" href="/about">Read more</a>

要是想通过普通的 .button 选择器来设置按钮样式,此时就会遇到问题:该选择器的优先级为 0, 1, 0,低于 a:any-link0, 1, 1。因此 .button 中的任何样式都无法覆盖上述基础样式中的字体颜色和加粗效果。解决这一问题的传统方法是调整其中一个选择器的优先级,或者两个同时调整,以达到期望的效果。

8.1.1 图层的定义

Defining layers

针对上述问题,现代 CSS 采取的更为有效的管理方式是使用 层叠图层(cascade layers [1]层叠图层 是层叠规范新增的一项标准,它能将样式划分到一系列称为 图层(layers 的分组结构中;而这些图层的优先级顺序可以任意指定。这样一来,图层 A 内的所有样式就可以无视选择器中的优先级顺序,始终先于图层 B 中的样式进行显示。

层叠图层 早在 2022 年初就开始在所有的主流浏览器中发布,因此得到了相当广泛的支持。不过保险起见,还是应该访问 https://caniuse.com/css-cascade-layers 来查看最新的浏览器支持情况。由于层叠图层很难在不支持该功能的浏览器中实现优雅降级,对于一小部分用户而言,可能会因为无法立即更新浏览器而无缘体验层叠图层带来的便利——旧版浏览器将忽略图层内的所有样式规则。

根据您阅读本文的时间和目标受众的不同,这样的不兼容问题可能可以接受,也可能接受不了。因此在决定是否启用层叠图层时,请务必查看最新的浏览器兼容性情况。如果非要启用图层,却又担心浏览器存在兼容性问题,可以参考这篇文章给出的替代方案:https://mng.bz/z89w,了解更多可用于模拟 CSS 图层效果的工具详情。

图 8.1 为两个来自不同层叠图层的样式效果。无论选择器的优先级如何,位于高优先级图层中的任何样式都将覆盖低优先级图层中的样式。本例中,选择器 .button 中的样式位于优先级更高的图层,因此将覆盖位于低优先级图层的、由选择器 a:any-link 指定的样式声明。

图 8.1 优先级较高的图层中的样式,将覆盖优先级较低的图层中存在冲突的样式声明
图 8.1 优先级较高的图层中的样式,将覆盖优先级较低的图层中存在冲突的样式声明

图层(Layers) 的定义通过 @规则中的 @layer 来实现。图层上的样式则写在相应的 @layer 规则的大括号内,如代码清单 8.1 所示。该样式代码定义一个全局图层(global layer)和一个主题图层(theme layer)。由于样式表中后定义的图层优先级将 高于 先定义的图层,因此,当页面样式存在冲突时,主题图层内的样式声明将优先于全局图层进行渲染,而与选择器原有的优先级无关。

代码清单 8.1 层叠图层的样式语法示例

@layer global { /* 定义一个全局图层 */
  /* 以下均为该全局图层所属样式 */
  :root {
    --brand-blue: #0063cc;
  }

  a:any-link {
    color: var(--brand-blue);
    font-weight: bold;
  }
}
@layer theme { /* 定义一个主题图层 */
  /* 以下均为该主题图层所属样式 */
  .button {
    display: inline-block;
    padding: 0.5rem;
    color: white;
    background-color: var(--brand-blue);
    font-weight: normal;
    text-decoration: none;
  }
}

因此,<a class="button"> 元素最终将按常规粗细、蓝底白字进行渲染。

在层叠顺序上,层叠图层位于样式表来源(origin)及行内样式(inline styles)之后、选择器优先级(specificity)之前。同一图层内,所有的常规优先级规则仍然适用。图 8.2 给出了第 1 章中曾经介绍过的 CSS 层叠顺序示意图。图中清晰展示了作用于同一元素上的、两个相互冲突的样式声明在先后顺序上的判定过程。

图 8.2 在层叠规则的顺序判定中,层叠图层位于选择器优先级的前面
图 8.2 在层叠规则的顺序判定中,层叠图层位于选择器优先级的前面

警告

未在层叠图层中定义的样式将优先于图层中的样式。这些样式可以理解为位于自身固有的最高优先级图层内。

您还可以对层叠图层命名。图层名称可以是由字母或数字字符(alphanumeric characters)、连字符(hyphens)及下划线(underscores)构成的任何有效标识符;图层名称不能以十进制数字开头;除此之外的任何字符都必须用反斜线进行转义(如 @layer theme\$)。也就是说,您可以根据具体情况为层叠图层进行命名并构建出相应的结构来。稍后我将介绍一个常用模式(common pattern)来对这些命名图层进行合理组织。

1 匿名图层

Anonymous layers

层叠图层也可以不用命名。如果省略图层名称,则默认创建了一个 匿名图层(anonymous layers。代码清单 8.2 给出了代码清单 8.1 的匿名图层版本,二者效果相同。

代码清单 8.2 匿名层叠图层的写法

@layer { /* 第一个匿名图层 */
  :root {
    --brand-blue: #0063cc;
  }

  a:any-link {
    color: var(--brand-blue);
    font-weight: bold;
  }
}

@layer { /* 第二个匿名图层 */
  .button {
    display: inline-block;
    padding: 0.5rem;
    color: white;
    background-color: var(--brand-blue);
    font-weight: normal;
    text-decoration: none;
  }
}

上述代码中,第二个图层仍然优先于第一个图层,因为前者是在样式表的后面定义的。

在只需要几个图层的简易网站中,采用匿名图层可能也没什么问题,但通常不推荐这种方案。图层名称有利于该图层的反复引用,以便在不同位置对同一图层设置样式。因此,命名图层的灵活性通常会比匿名图层更高,稍后会演示说明。另外,图层名称在传达各个图层的意图(purpose)方面也有可圈可点之处,可以进一步增强代码的逻辑性。

说明

在使用 @规则中的 @import 来导入外部样式表时,也可以将目标样式表指定给某个图层。例如,使用 @import "bootstrap.css layer(bootstrap);" 就能将样式表 bootstrap.css 导入到一个名为 bootstrap 的图层中,效果上就如同将导入的样式表所有内容都放进 @layer 规则中一样;但通过 HTML 的 <link> 标签来关联样式表时,可惜还没有类似的写法。如果非要在 HTML 中通过这种方式直接导入 CSS 样式,可以考虑在 <style> 标签中使用 @import 规则。

8.1.2 Layer 图层的顺序与优先级

Layer order and priority

Layer 图层为人们提供了一种确定 CSS 样式表各部分优先级的方法。CSS 样式表往往是从页面整体的通用样式开始设计的,例如字体和各种颜色的设置;然后逐渐过渡到页面特定部分的样式,比如标题栏或菜单区等。

在此期间,使用图层将带来两个好处:首先,对比选择器的优先级判定规则,Layer 图层提供了更为灵活、也更为直观的方式来操控 CSS 样式渲染的先后顺序;其次,一组命名良好的图层可以更好地传达样式设计的意图,极大地方便了后续的修改。例如,一个命名为 reset 的图层可能会更专注于替换某些浏览器默认样式;而命名为 global 的图层则可能更关注页面整体的字体设置与颜色搭配。

层叠图层的优先级是基于该图层在样式表中首次出现的顺序来确定的。如果图层重复声明,则第二次声明不会改变其优先级,如代码清单 8.3 所示。以下样式代码的效果将与图层重复声明前完全一致。由于 global 全局图层率先声明,因此其优先级也低于后面的 theme 主题图层;对于第二个 @layer global { ... } 块中的全局样式也是如此。

代码清单 8.3 引用两次同一图层的示例样式代码

@layer global { /* 建立 global 全局图层 */
  :root {
    --brand-blue: #0063cc;
  }
}

@layer theme {
  .button {
    display: inline-block;
    padding: 0.5rem;
    color: white;
    background-color: var(--brand-blue);
    font-weight: normal;
    text-decoration: none;
  }
}

@layer global { /* 给 global 全局图层添加更多样式 */
  a:any-link {
    color: var(--brand-blue);
    font-weight: bold;
  }
}

再次引用相同的图层名称,将向样式表中已经建立起来的该图层添加更多样式。

通常,最好就需要哪些图层、以及这些图层遵循什么样的先后顺序提前做好规划。有了具体的图层规划后,就可以先声明多个图层,然后再分别给它们添加样式。具体做法是:在单个 @规则 @layer 中依次罗列各图层的名称,并以英文逗号进行分隔;书写时无需带上大括号。例如以下样式代码,按优先级逐级递增的顺序先后声明了四个图层:

@layer reset, global, theme, components;

如图 8.3 所示,本例中添加到 global 全局图层的所有样式,都将无差别覆盖 reset 重置图层中、存在冲突的任一样式声明;而添加到 theme 主题图层的所有样式,则会同时覆盖 reset 重置图层与 global 全局图层中对应的样式;最后,components 组件图层中的样式将覆盖前面三个图层中的所有冲突样式。再次强调:该声明 必须写在 给各图层添加任何样式 之前,因为层叠图层的优先级 是由该图层在样式表中首次声明的位置决定的

图 8.3 示例中四个图层的先后顺序示意图
图 8.3 示例中四个图层的先后顺序示意图

强烈建议在样式表的一开始就声明所有的层。这样既明确了它们各自的先后顺序,同时也便于后续查找。完成声明之后,才是使用不同的 @layer 代码块,将您设计的样式分别嵌入这些事先定义好的图层中。

另外,对于所有不在任何层叠图层中的样式声明,其优先级始终高于那些在图层中声明的样式。这些样式其实是被当成处于享有最高优先级的图层内。因此,建议将所有的样式都放入某个图层中。

提示

没有所属图层的样式(unlayered styles)所具有的最高优先级或许在临时调试或原型设计时很有用;可一旦确定了需要的具体样式,就应该将其划分到适当的层叠图层中。

8.1.2.1 在图层中使用 !important 标记

Using !important in layers

本书第一章介绍过,CSS 有三种样式表来源(origin):用户代理(user-agent)样式表、用户(user)样式表、以及作者(author)样式表。并且还提到过,对于添加了 !important 标记的 CSS 样式,其来源的优先级顺序是反转的。虽然作者样式的优先级高于用户代理样式,但添加了 !important 标记的用户代理样式的优先级反而会高于同样标记了 !important 的作者样式。

层叠图层也有类似的行为特征。再来看一下这行样式代码,它定义了四个按优先级递增排列的图层:

@layer reset, global, theme, components;

此时,components 组件图层的样式优先级高于 global 全局图层;但是 global 全局图层上标注为 !important 的样式的优先级反而会高于 components 组件图层中同样标注了 !important 的样式,如图 8.4 所示:

图 8.4 标注了 !important 的样式,其图层优先级的顺序和没标记时是相反的
图 8.4 标注了 !important 的样式,其图层优先级的顺序和没标记时是相反的

这个功能特性您可能不怎么用到,但在某些情况下则会变得很有用。代码清单 8.4 模拟了一个类似场景。该代码利用媒体查询 prefers-reduced-motion 来检测用户是否在浏览器和操作系统重减少了动画效果。这些具体的样式声明将在本书最后几章介绍过渡与动画效果时进行讲解。但说到底,它们会响应那些对动画效果有特定敏感性的用户设置,并禁用动画效果。

代码清单 8.4 利用 !important 来减少动画效果

@layer reset {
  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

这些声明是在优先级最低的 reset 图层上定义的,因为从概念上讲,它们属于这一图层;但由于标注了 !important 标记,这些样式将比后续图层中的所有样式都具有更高的优先级,即便后续涂层的样式也标注了 !important 标记。

在过去,人们往往尽量避免使用 !important,因为它可能导致样式优先级的“军备竞赛”等问题;而如今随着层叠图层对样式优先级的控制加强,!important 也可以变为一个非常有用的调控工具。

8.1.2.2 Layer 图层的嵌套

Nested layers

偶尔人们也想对图层的组织有更为细致的操控,于是引入了 嵌套图层(nested layers 等相关概念。

嵌套图层可以通过把一个图层嵌入另一个图层中来实现。例如:

@layer modules {
  @layer layout { ... }
  @layer theme { ... }
}

AI写代码css
1
2
3
4

上述示例代码中,modules 模块图层被进一步细分为 layout 布局图层与 theme 主题图层。在 modules 图层中,各图层的优先级规则仍然适用,因此 theme 主题图层的优先级高于 layout 布局图层;而位于 modules 模块图层中、且不在嵌套图层内的样式声明,其优先级将同时高于 layout 布局图层与 theme 主题图层。

此外,还可以使用 句点表示法(dot notation 来定义嵌套图层。以下代码为上述示例的等效版写法:

@layer modules.layout { ... }
@layer modules.theme { ... }

AI写代码css
1
2

该句点表示法极大地简化了嵌套图层的后续引用,并能根据设计需要对 @layer 规则作进一步嵌套,或者用更长的句点标识符来描述图层的嵌套情况。例如写作 @layer modules.layout.base 来嵌套更多图层。

嵌套图层的另一个好处是方便将第三方样式表直接导入某个图层中,而不必担心给该样式表中的其他预定义图层带来干扰;而如果导入的样式表中定义了任意图层,它们都将一并嵌入 @import 语句所指定的层叠图层内。

8.1.3 关键字 revert-layer 的用法

The revert-layer keyword

在本书开篇时,我们学习了可以在任意样式属性(property)上声明的四个重要关键字:initialinheritunset 以及 revert。对于层叠图层而言,还多了一个关键字:revert-layer

回顾第一章介绍的内容,revert 关键字可以撤回设置在作者样式表中的任意属性值,并将其恢复为浏览器默认样式。同理,revert-layer 关键字则可以撤回设置在当前图层上的任意样式,并将其恢复为此前图层中对应的值。

这在当您定义了一组全局样式、且在优先级更高的图层中覆盖了其中的部分样式时非常有用。此时使用 revert-layer 关键字就能恢复这些被覆盖的全局样式。

下面用一个例子演示说明。将代码清单 8.5 中的样式添加到样式表,并关联到 HTML 页面。和之前类似,这段代码先在 global 全局图层中定义了某些链接样式;然后在 theme 主题图层中又定义了一些特殊的按钮样式;最后,使用 revert-layer 关键字将 .blog-content 样式类中的链接样式恢复成全局样式。

代码清单 8.5 利用 revert-layer 关键字恢复链接的全局样式

@layer global, theme;

@layer global {
  :root {
    --brand-blue: #0063cc;
  }

  a:any-link {
    color: var(--brand-blue);
    font-weight: bold;
  }
}

@layer theme {
  .button {
    display: inline-block;
    padding: 0.5rem;
    color: white;
    background-color: var(--brand-blue);
    font-weight: normal;
    text-decoration: none;
  }

  .blog-content a:any-link {
    /* 将 .blog-content 内的所有链接恢复成全局图层中定义的样式 */
    display: revert-layer;
    padding: revert-layer;
    color: revert-layer;
    background-color: revert-layer;
    font-weight: revert-layer;
    text-decoration: revert-layer;
  }
}

保存代码后,无论 button 样式类是否存在,容器 <div class="blog-content"> 中的的链接元素都会按此前定义在 global 全局图层中的样式进行渲染。

8.1.3.1 属性 all 的使用

The all property

像刚才那样逐一还原多个属性,写起来可能会很繁琐。这时可以使用一个简便的 all 属性来一次性还原所有需要撤销的属性样式,写作:

@layer theme {
  .blog-content a:any-link {
    all: revert-layer;
  }
}

注意,all 属性(property)并非层叠图层的专用属性,只是用在这里恰到好处。all 的合法属性值仅限于这五个特殊关键字:initialinheritunsetrevert 以及本节介绍的 revert-layer

8.2 层叠图层的推荐组织方案

A recommended organization for cascade layers

层叠图层为 CSS 提供了极大的灵活性。所谓成也萧何败也萧何,这样的灵活性也可能让人很难做出抉择:究竟该怎样明确组织代码并分清各部分的先后主次呢?

层叠图层是一项新推出的功能特性,还有很多实验空间可供探索,但业内已经出现了一些通用的主题,比如下面这组图层列表:

@layer reset, theme, global, layout, modules, utilities;

其他开发者也推荐过非常类似的写法,上述代码则是根据前端工程师 Stephanie Eckles 推荐编制而成的。这些图层在页面上的堆叠情况如图 8.5 所示,位置靠后的图层的优先级高于靠前的图层:

图 8.5 关于合理组织图层的一个实用模板示意图
图 8.5 关于合理组织图层的一个实用模板示意图

接下来将逐一考察各个图层,看看它们是如何帮助您更好地组织样式代码的。我们将从优先级最低的 reset 重置图层开始介绍。注意,并非每个项目都必须包含上述每个图层,可根据实际情况略作删减。

如果想跟着一起练练手,也可以创建一个新的样式表,并跟随本书的讲解同步添加样式。本章不会再手把手带您构建某个具体的页面,而是更注重代码本身的组织方式。建议将所有示例样式放在同一个地方,以便观察它们的组合方式,并在浏览器中验证实验效果。

8.2.1 Reset 重置图层

Reset layer

在 CSS 的早期阶段,不同的浏览器对应的某些用户代理样式差异很大。因此,通过页面样式重置来让链接、列表、标题等元素的行为可控就显得尤为重要了。目前线上可用的重置样式版本有好几个,其中最受欢迎的一个,叫做 normalize.css

尽管当前浏览器的趋同性还在进一步增强,不再需要这些重置样式来修复彼此间的不一致问题,但将某些属性重置为更为理想的默认值还是很有必要,包括边框盒模型的设置(详见第 3 章)、调整按钮及 input 文本框的默认字体、以及去掉 <body> 元素上默认的少量外边距等。

将代码清单 8.6 中的样式添加到本地示例样式表,为所有图层确立先后顺序,并设置一些常见的重置样式。

代码清单 8.6 reset 重置图层的样式代码示例

@layer reset, theme, base, layout, modules, utilities; /* 确立所有图层的先后顺序 */

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box; /* 盒模型按边框盒计算尺寸 */
  }

  body {
    margin: unset; /* 移除 body 的外边距 */
  }

  button,
  input,
  textarea,
  select {
    font: inherit; /* 修复表单元素的字体行为 */
  }

  img,
  picture,
  svg,
  canvas { /* 提升图片及类似元素尺寸的可预见性 */
    display: block;
    max-inline-size: 100%;
    height: auto;
  }

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

以上是我推荐的样式代码,之前的章节已经涵盖了大部分内容。这里只是将我的意见汇集起来,提供一个实用参考。您可以根据需要适当修改。

reset 重置图层通常不需要太多样式,其主要作用是将用户代理样式 “修复(fix)” 成您喜欢的默认样式。所有基于该页面的样式设计都将在后续图层予以体现。使用时可将该层样式完整复制到各个项目中,视具体情况略作调整即可。

8.2.2 Theme 主题图层

Theme layer

接下来需要定义的是自定义属性,尤其是页面将会用到的各种颜色。从概念上讲,提前考虑这些自定义属性是有意义的,以便被后续样式引用;指定较低的优先级也是合理的,这样一来就能在某些位置轻松覆盖掉它们。

除了自定义属性外,还有两个属性也应该在这一层处理。第一个是 accent-color,它用于自定义某些元素的强调色,例如选中的复选框及单选按钮、范围输入组件(<input type="range">)和进度条(<progress>)元素。将其设置为与页面其他颜色相匹配的颜色,能为您的网站增色不少。

第二个属性是 color-scheme,可指定为 lightdark,让页面在浅色模式与深色模式间切换。浏览器会读取该设置来确定表单输入框和滚动条的合理的默认颜色(具体效果视浏览器的不同而各异)。将代码清单 8.7 中的样式添加到本地样式表,为页面设置主题层样式。

代码清单 8.7 theme 主题图层样式代码示例

@layer theme {
  :root {
    --brand-color: #0063cc;
    --background-color-1: #edf3fa;
    --background-color-2: #c6cdd5;
    --foreground-color-1: #39434d;
    --foreground-color-2: #5a636d;
    --font-main: "Helvetica Neue", "Nimbus Sans", Arial, sans-serif;
    --font-heading: Georgia, sans-serif;

    accent-color: var(--brand-color);
    color-scheme: light;
  }
}

以上代码只是演示主题图层的用法;若是在大多数现实世界的网站中,则需要设置更多的颜色。比如我的个人博客中的样式表就定义了超过 50 各自定义属性;而我参与开发的应用包含的则多达数百个。我将在后续章节介绍一些命名和颜色管理的实用建议,以便在样式表中进行相关操作。

注意:并非所有自定义属性都要在这一层定义。theme 主题图层可视为自定义属性的大本营,但页面某些地方可能需要一些无需全局生效的自定义属性。这种情况下,将这些自定义属性和用到它们的代码放在一起通常更为合理。例如,如果一个侧边栏会用到一个专属的 --image-radius 属性,那么就可以考虑将其与侧边栏的样式放到一块儿。这些样式可能位于优先级更高的图层,例如 layout 布局图层或者 modules 模块图层。

8.2.3 Global 全局图层

Global layer

层叠图层的第三层为 global 全局图层(也称 base layer,即 基础图层)。该层用于设置对整个网站生效的所有默认样式,包括默认字体样式、背景色与字体颜色、标题与链接样式等。

这一图层的大多数选择器都是标签选择器(tag selectors),因为它们是在各种 HTML 元素上生效的全局默认样式。代码清单 8.8 给出了全局样式的一个示例,将它们添加到本地样式表:

代码清单 8.8 global 全局图层样式代码示例

@layer global {
  :root {
    font-size: clamp(1rem, 0.4rem + 0.8svw, 1.2rem);
  }

  body {
    font-family: var(--font-main);
    background-color: var(--background-color-1);
    color: var(--foreground-color-2);
  }

  a:any-link {
    color: var(--brand-color);
  }

  h1 {
    font-family: var(--font-heading);
    font-size: 2.2rem;
  }

  h2 {
    font-size: 1.125rem;
  }

  @media (min-width: 768px) {
    h1 {
      font-size: 3rem;
    }

    h2 {
      font-size: 2rem;
    }
  }
}

上述代码使用了定义在 theme 主题图层上的自定义属性,同时还设置了一些字号以及一些响应式行为,以便在更宽的视口上增大字号。其他还可以在 global 全局图层上定义的样式包括:表单 input 文本输入框、label 标签、代码块(code blocks)、大段引用(blockquotes)、以及像 :hover:visited 这样的链接状态的进一步处理。

8.2.4 Layout 布局图层

Layout layer

由外向内地设计页面样式往往是最容易实现的。在专注于内部元素的微调之前,先大致确定页面的主要部分——这便是 layout 布局图层的主要目的。

layout 布局图层专注于更高层面的页面布局,包括页眉页脚样式、侧边栏样式布局等等。如果需要在大型网格中放置一系列元素项或者定义列的样式,也可以将它们放在这一层。

很多网站会提供好几种不同的页面布局。例如,主页的布局可能与文章正文布局不同;而文章正文布局又与搜索结果页面布局不同。您可以在 layout 布局图层定义每个页面的总体结构。

代码清单 8.9 给出了部分该图层样式的简要示例。将这些样式添加到您本地的样式表中并查看它们的上下文,即主页布局和文章页布局的一些特定样式。

代码清单 8.9 layout 布局图层的样式代码示例

@layer layout {
  #homepage {
    display: grid;
    grid-template-areas:
        "header header"
        "main sidebar"
        "footer footer";
    grid-template-columns: 1fr 300px;
    gap: 1rem;
  }

  #homepage > header,
  #homepage > footer {
    grid-column: span 2;
  }

  #article > main {
    max-inline-size: 1000px;
    margin-inline: auto;
  }
}

您可以将 ID 值 homepagearticle 添加到页面的 <body> 元素或者更上层的 <div> 元素来切换页面生效的布局。这样,同一份样式表就能在多个页面间重用。

过去,人们通常对选用 ID 选择器非常谨慎,因为它的优先级太高;随着层叠图层的出现,这类问题将迎刃而解。如有必要,添加到后续图层中的任意样式都能根据图层的优先级原理轻松覆盖当前图层内的样式规则。如果还是用不惯 ID 而选用样式类,推荐一个常见的样式类写法:使用 l- 前缀(L 即 layout,表示布局的意思)。例如,上述样式代码中的 ID 选择器可以分别替换为 .l-homepage.l-article。使用 l- 前缀可以迅速区分出普通样式类与层叠图层中的样式类。

还有一点特别重要:根据 HTML 的规则,页面上的 ID 必须是唯一的。因此,如果页面上存在重复使用的布局样式,应该首选样式类来完成相关样式的设置。

8.2.5 Modules 模块图层

Modules layer

接下来的这个层叠图层专注于页面的可重用单元,称为 modules 模块图层。这些模块通常也被称为组件(components)、区块(blocks)或对象(objects)。它们通常包括下拉菜单(dropdown menus)、模态框(modals)、横幅图片(banner images)、导航栏(navigation bars)以及信息卡片(information cards)等。模块图层中的样式主要为页面各部分提供大部分颜色、间距、排版规则等。绝大多数样式都位于这一层。

作为通用规则,各模块应该有一个唯一的名称,并且名称应该相对简短,以便在页面任意位置进行添加。代码清单 8.10 给出了 nav-menu 模块与 card 模块的一组示例样式。将它们添加到您本地的样式表中:

代码清单 8.10 modules 模块图层的示例样式代码

@layer modules {
  .nav-menu { /* 导航菜单模块 */
    margin-block: unset;
    padding-inline: unset;
    border: 1px solid #ccc;
    list-style: none;
  }

  .nav-menu > li + li {
    border-top: 1px solid #ccc;
  }

  .nav-menu > li > a {
    display: block;
    padding: 0.8em 1em;
    color: inherit;
    font-weight: normal;
  }

  .nav-menu > li > a:hover {
    color: var(--brand-color);
    background-color: white;
  }

  .card { /* 卡片模块 */
    padding: 1rem;
    border-radius: 0.5rem;
    background-color: #fff;
  }

  .card > h3 {
    align-self: end;
    margin-block: 0;
    padding-block-end: 0.5rem;
    border-block-end: 1px solid #eee;
  }
}

构建一个由可重用模块组成的网站是一个独立的大主题。我们将在下一章进行更深入的探讨。

鉴于 modules 模块图层是包含样式代码最多的图层,将其进一步细分为各个嵌套图层可能更为实用。但层叠图层目前仍然是一个相对较新的功能,因此暂时还没有任何具体的常用策略。

8.2.6 Utilities 工具图层

Utilities layer

偶尔可能也需要通过一个样式类来实现一个既简单而特别具体的效果,比如设置文本居中或隐藏某个元素等。这些样式类也被称之为 工具类(utilities classes)。

工具类有点类似小型的模块,然而其实现的效果应该非常聚焦。工具类上的样式声明通常不会超过一个。样式清单 8.11 给出了定义在 utilities 工具图层上的部分工具类样式。每个工具类都将执行某个特定的操作:文本居中、元素隐藏、设置圆角半径等等。将这些示例代码添加到您的样式表中:

代码清单 8.11 utilities 工具图层的示例样式代码

@layer utilities {
  .text-center {
    /* 令文本在容器内居中对齐 */
    text-align: center;
  }

  .hidden {
    /* 隐藏页面上的某个元素 */
    display: none;
  }

  .border-radius {
    /* 为某元素设置圆角半径 */
    border-radius: 1rem;
  }
}

过去我通常会在大部分工具类上添加 !important 标记;现在有了层叠图层,只需将它们放到优先级更高的图层上就能达到同样的效果。无论在哪里应用该工具类,它都会生效。当您想在某个元素上设置文本居中效果、却又不希望它被其他样式覆盖时,我向您保证,只要给该元素添加一个 text-center 的样式类就行了。

工具类旨在成为您的得力助手,让您无需引入一个完整的模块就能轻松实现页面上的一个简单效果。工具类虽好,但也不要过于依赖它们。在大多数网站开发中,您可能会要用到的工具类通常也就十来个。

8.2.7 其余图层

Additional layers

一些开发者倾向于再加一个第七层,并称之为 states 状态图层,旨在根据各种模块的状态(state)或扮演的角色(status)动态地添加或删除页面上的某些样式。这些样式可以是页面加载状态、下拉菜单的打开或关闭状态等。

我个人倾向于将它们和 modules 模块图层放到一起,或者和它的嵌入图层放到一起。具体情况将在下一章进行重点考察。

最后再次强调,以上介绍的特定图层组织结构仅仅只是一种推荐方案,并非硬性要求。在开发您自己的图层应用模式时,完全可以根据具体需求随意调整;您也有可能设计出更对您口味的组织方案,或者让某些项目完全受益于另一套方法论。

8.3 伪类 :is():where() 的用法

The : is () and : where () pseudo-classes

CSS 有两个相对较新的伪类选择器可用于代码的组织::is():where()。先来看看 :is() 伪类。

该伪类可以让大量选择器的书写变得简洁高效。为演示说明,考虑以下未使用 :is()CSS 样式代码:

.contact-form input,
.contact-form textarea,
.contact-form select,
.contact-form button {
  padding: 5px 10px;
  border: 1px solid var(--brand-color);
  border-radius: 5px;
}

这段样式代码中,选择器存在明显重复,并且可读性也不强;而伪类 :is() 可以将所有内容合并为一个选择器,在实现同样效果的基础上让代码更加简洁明了。以下代码同之前的样式等效:

.contact-form :is(input, textarea, select, button) {
  padding: 5px 10px;
  border: 1px solid var(--brand-color);
  border-radius: 5px;
}

准确地说,:is() 伪类是一个函数,它接受一系列选择器作为参数,并匹配所有与给定选择器中的任意一个匹配的元素。

:is() 的优先级是由传入参数的最高优先级决定的。因此,:is(input, textarea, select, button) 的优先级为 0, 0, 1;而 :is(input, #login-password) 的优先级则为 1, 0, 0。无论最终匹配到的是哪个选择器参数,:is() 的优先级都固定不变。

警告

:is() 伪类选择器的参数不能是伪元素选择器(pseudo-element selectors)。所以类似 :isbefore, div::after) 这样的写法是无效的;另一方面,传入另一个伪类作参数则是合法的。例如,写作 :is(:first-child 则是有效的。

8.3.1 更宽容的选择器

More forgiving selectors

:is() 的另一个好处是它对无效或未识别的选择器更加宽容。考虑以下选择器:

input.invalid,
input:user-invalid {
  border: 1px solid red;
}

上述代码中,伪类 :user-invalid 相对较新,浏览器支持有限,因此在某些浏览器中,即使其他选择器匹配成功,整个选择集也会被忽略。这一限制偶尔会让开发者感到意外;为此,:is() 在设计时便尤为注重其对无效选择器的宽容性。上述代码可以改成下面这样,实现更符合预期的效果:

input:is(.invalid, :user-invalid) {
  border: 1px solid red;
}

按照这样修改后,不支持 :user-invalid 伪类的浏览器仍然可以让匹配样式类 .invalid 的部分生效,从而在样式表中以更优雅的方式实现新伪类的渐进式增强。

8.3.2 对优先级的调控

Manipulating specificity

:where() 伪类在功能上与 :is() 完全相同,唯一的区别在于 :where() 的优先级 始终为零。也就是说 :where() 可用于调低之前优先级较高的选择器。

假设想根据 ID 值来选中元素,但又不希望它覆盖掉同一图层(layer)上的其他样式,就可以编写一个像这样的选择器::where(#login-form) input。该选择器的优先级为 0, 0, 1 —— 它是 :where(#login-form) 的优先级 0, 0, 0input 的优先级 0, 0, 1 的结合。此外还可以将多个 :where() 伪类链接在一起(译注:如 :where(.class1) :where(.class2) { ... })。

在本章之前的一个示例中,我们希望用 .button 来覆盖选择器 a:any-link。这里也可以使用 :where() 伪类实现相同的效果,如代码清单 8.12 所示。当按钮样式(button styles)应用于同一元素时,链接样式(link styles)将被覆盖。

代码清单 8.12 利用 :where() 来调低优先级

a:where(:any-link) { /* 优先级为 0,0,1 */
  color: var(--brand-blue);
  font-weight: bold;
}
.button { /* 优先级为 0,1,0 */
  display: inline-block;
  padding: 0.5rem;
  color: white;
  background-color: var(--brand-blue);
  font-weight: normal;
  text-decoration: none;
}

在层叠图层推出以前,这是一种控制冗长选择器优先级的非常有用的写法;虽然目前 :where() 的使用频率不高,但在某些场合,尤其是在管理位于同一图层上的样式时,仍然很有用。

同理,也可以给 :is() 选择器传入一个优先级较高的选择器参数来有意调高该选择器的整体优先级。例如 :is(.button, #fake-id) 就人为创建了一个优先级为 1, 0, 0 的选择器;但我并不推荐这样写,毕竟选择器优先级还是应该尽量保持低位运行。

当前内容所在位置(可进入专栏查看其他译好的章节内容)

《CSS in Depth》新版封面

《CSS in Depth》新版封面

译者按
因为临时有事耽搁,有段时间没更新专栏了,今天赶紧补上。原来书中的嵌套(nesting)并非是针对层叠图层(cascade layer)而言的,而是 CSS 新推出的一个语法糖(莫非是迫于 Sass 和 Less 的强大攻势?哈哈),还是等学完这一章再来统一更正前面的说法吧。对一个事物的认识就是这样螺旋上升的。一起学起来!

嵌套(Nesting) 是 CSS 的一项功能。它可以将一个样式规则放入另一个样式规则内,从而使子规则的选择器与父规则的选择器相对应,既减少了代码冗余,又可以将相关选择器组合到一起。如果之前用过 SassLess 等 CSS 预处理工具,就不会对这种写法感到陌生(不过也存在一些细微差别,稍后会详述)。

警告

嵌套是一项全新的功能。截至 2023 年底,嵌套已经在所有的主流浏览器中启用,不过部分用户可能需要过段时间才能安装升级。在决定是否在生产环境中使用嵌套特性前,请参考 https://caniuse.com/css-nesting 了解最新的使用情况统计信息。

下面举例说明嵌套功能的用法。我将为您展示一个同时启用嵌套功能和不启用的对比示例。如代码清单 8.13 所示,该样式代码为传统非嵌套的三个关联规则集。注意,这三个选择器都以相同的样式类 .card 开始。

代码清单 8.13 不带嵌套功能的 CSS 相关选择器

.card {
  padding: 1rem;
  border-radius: 0.5rem;
  background-color: #fff;
}

.card > h3 {
  margin-block: 0;
  border-block-end: 1px solid #eee;
}

.card .card-body {
  padding-block: 1em;
}

AI写代码css
1
2
3
4
5
6
7
8
9
10
11
12
13
14

而用上嵌套后的代码将更简洁。代码清单 8.14 为上述代码的等效嵌套版。样式类选择器 .card 从第二、第三个选择器中移除,取而代之的,是将它们嵌套进第一个规则集内:

代码清单 8.14 嵌套 CSS 选择器

.card { /* 父级选择器 */
  padding: 1rem;
  border-radius: 0.5rem;
  background-color: #fff;

  > h3 { /* 选中 .card > h3 */
    margin-block: 0;
    border-block-end: 1px solid #eee;
  }

  .card-body { /* 选中 .card .card-body */
    padding-block: 1em;
  }
}

AI写代码css
1
2
3
4
5
6
7
8
9
10
11
12
13
14

浏览器会将被嵌套的规则集选择器与外层选择器连接起来,因此其选中元素仍然与未使用嵌套的上一个版本相同。这样做有两个好处。首先,它消除了 .card 在所有三个选择器中的代码冗余,便于后续对该样式类进行修改、且更不容易出错;其次,嵌套的写法明确将相关样式归入同一组大括号内,表明它们都是协同工作的。其中的缩进是可选的,但一般都会保留,旨在强调代码结构。

而选择器的优先级则由最终组合的选择器共同决定,因此父选择器和子选择器的优先级是直接累加的。选择器 .card-body 的优先级加上其父选择器 .card 的优先级,即为 0, 2, 0。换句话说,嵌套写法中的样式与没有嵌套的版本具有相同的优先级。

提示

尽量避免让选择器嵌套过深,以免让选择器的优先级远远超出实际需要。

如果往子规则集添加更多规则集,就能将样式嵌套进更深的层中;不过这样一来,就像书写更长的选择器一样,最终组合选择器的长度也会相应增大,从而导致优先级过高。建议将规则集的嵌套深度控制在三层以内。如果超过三层,可能就需要考虑重新组织一下代码了。下一节将深入探讨这个问题。

8.4.1 嵌套选择器的使用

Using the nesting selector

除了默认的从属关系外,嵌套还可以用于表示其他结构,例如下面这段没有嵌套的样式代码:

.modal {
  display: none;
}
.modal.is-open {
  display: block;
}

该样式中,样式类 modal 显然重复了,但如果改为嵌套的写法,其默认行为将与想要的效果大相径庭。具体来说,要是将 .is-open 嵌入 .modal 代码块中,相当于写成了 .modal .is-open,中间隔了一个空格(即后代选择器);而我们期望的结果应该是 .modal.is-open

不过,只要在内部选择器上用一个 & 连接符来指代外部选择器,就能轻松解决这个问题。这里的连接符 & 也被称为 嵌套选择器(nesting selector)。嵌套选择器可以引用外层选择器,并将其合并到当前内层选择器中。再来看下面这个案例,其中内层选择器 &.is-open 等同于 .modal.is-open,两个样式类选择器间没有空格。

代码清单 8.15 利用 & 来构建一个复合选择器

.modal {
  display: none;

  &.is-open { /* 将选中 .modal.is-open */
    display: block;
  }
}

AI写代码css
1
2
3
4
5
6
7

若要针对页面元素的多种变体或状态应用不同的样式,嵌套选择器就会非常好用。例如使用 &:hover 作内部选择器,选中悬停状态下的外层元素;或者使用 &::after 来选中外层选择器的伪元素 ::after

注意

CSS 中的 & 无法像 Sass 那样,通过嵌套选择器拼接出一个新的类名。在 Sass 中,父级为 .card 的内部选择器 &large 会选中一个拼接后的样式类 .cardlarge;而在 CSS 中则会被解析为 large.card,选中一个并不存在的 <large class="card"> 元素。

& 还可以用于调整选择器顺序,写到嵌套选择器的后面,如代码清单 8.16 所示。在这段样式代码中,嵌套选择器 .homepage & 将选中所有满足 .homepage .card 条件的元素,即位于元素 .homepage 内部的所有 .card 元素。

代码清单 8.16 在嵌套的 CSS 样式中使用 & 符号

.card {
  padding: 1rem;
  border-radius: 0.5rem;
  background-color: #fff;
  .homepage & { /* 选中 .homepage .card */
    background-color: #ccc;
  }
}

AI写代码css
1
2
3
4
5
6
7
8

这种写法某种意义上实现了代码的反转(flips the code around):不必再为外层选择器的子元素设计样式,而是在特定上下文中直接给外层元素设计样式即可。

警告

通常情况下,应当尽量避免在选择器末尾使用 & 来嵌套样式。虽然并没有明令禁止这么写,但根据上下文来设计样式不利于样式代码的长期维护。具体原因留待下一章解释,届时会给出其他变通方案。

嵌套选择器一个废弃的早期写法

在嵌套规范的早期版本中,嵌套选择器必须以 &. 或者 # 符号开头,并且明令禁止以字母标识符开头(即标签选择器),因此,当时写成下列代码这样都是 无效的

.card {
  h3 { … }
}

AI写代码css
1
2
3

当时之所以加上这一条,是因为浏览器厂商担心嵌套选择器对其语言解析算法的性能有影响,并且在有效鉴别嵌套选择器和普通属性声明的能力方面信心不足。好在后来他们找到了有效的解决方案,该限制也就此作罢,上面的写法才重新成为有效写法。

之所以引出这一段往事,是因为许多早期教程都遵循了这个废弃的限制条件,并且 Chrome 浏览器和 Safari 浏览器的首批实现版本也确实执行了该限制条件。这种情况预计会在本书付梓前后有所改观。在此期间,可以在内部选择器上添加一个 & 来绕开这一废弃的规定(即写作 & h3)。

这些新的配置选项为 CSS 选择器的合理组织提供了更多新思路。具体如何组织是一个复杂的话题。下一章我会就如何以最佳方式组织选择器给出一些实用的建议,顺带提一下应该尽量避开的一些写法。

8.4.2 深入理解嵌套选择器

Understanding the nuances of the nesting selector

嵌套是 CSS 一个特殊的语言特性,因为从技术上讲,它并没有为 CSS 带来语言层面的新功能。这就是所谓的 语法糖(syntactic sugar:仅仅提供了一种让样式的书写更“甜美”(“sweeter”)的写法,使其更易于阅读或表达某些想法;而在底层,这样的写法则会被转换(或称 desugars,即去糖化处理)为不带嵌套的传统样式代码。

关键要明确一点:在使用 & 引用父级选择器时,转换或去糖化处理的本质,是将父级选择器封装到 :is() 伪类函数中。这样一来,外层选择器就可以写成包含多个选择器的列表形式,例如:

button,
input,
textarea,
select {
  &.invalid {
    border: 1px solid red;
  }
}

浏览器最终会将上述嵌套选择器解析为 :is(button, input, textarea, select).invalid。借助 :is() 来解释嵌套选择器,浏览器可以直接进行相关转换,即使是再复杂的深度嵌套也不例外。

8.4.2.1 嵌套对优先级的影响

Nesting affects specificity

在简单的示例中,嵌套对优先级的影响往往显而易见。之所以强调出来,主要是出于两个原因。一是嵌套写法会直接影响选择器的优先级。由于 :is() 伪类的优先级是由传入参数的最高优先级决定的,嵌套选择器也具有同样的行为特征。如果父级选择器具有较高的优先级,那么任何使用了 & 的选择器也将具有较高的优先级,例如以下示例样式:

input,
#login-form button {
  &:focus {
    border-color: var(--brand-color);
  }
}

此时,浏览器会将嵌套选择器解析为 :is(input, #login-form button):focus;即便需要匹配的选择器仅仅是 input:focus 那部分元素(其优先级单看为 0, 1, 1),最终的优先级却始终是 1, 1, 1

8.4.2.2 嵌套对目标元素的影响

Nesting affects which elements are targeted

使用嵌套写法的第二个影响是,最终匹配的目标元素可能比预想的要多。考虑以下嵌套选择器:

.card .title {
  footer & { … }
}

该嵌套选择器用于匹配 footer 元素内 .card 样式类下的 .title 标题,例如以下 HTML 元素:

<footer>
  <div class="card">
    <h3 class="title">Franklin Running Club</h3>
  </div>
</footer>

不过很容易漏掉的一点,是该选择器还可以选中以下 HTML 标记中的 .title 标题:

<div class="card">
  <footer>
    <h3 class="title">Franklin Running Club</h3>
  </footer>
</div>

嵌套选择器最终将转换为 footer :is(.card .title)。满足条件的 .title 既可以是 footer 的子项,也可以是 .card 的子项;而 footer.card 之间的从属关系并不明确,因此它们可以按任意顺序进行嵌套,选择器都会匹配成功。

这种细微差别很容易被忽视,而且很可能过了很长时间才会注意到,从而导致页面样式报错。一旦出现这样的情况,就必须意识到嵌套选择器相关的影响。

8.4.3 媒体查询及其他 @规则的嵌套

Nesting media queries and other at-rules

嵌套的写法还可以应用到媒体查询 @media 中,并且写起来非常方便,可能是因为我喜欢将相关代码组合在一起的缘故。这样一来,页面各个部分的响应式代码就可以放到一块儿了。

代码清单 8.17 就给出了一个示例版本。注意,嵌套媒体查询内部的样式声明不需要任何嵌套选择器;它们直接对父级选择器生效。这样,选择器只要写好一次,相应的响应式样式代码就会自动生效。

代码清单 8.17 嵌套媒体查询的示例样式代码

h1 {
  font-family: var(--font-heading);
  font-size: 2.2rem;
  
  /* h1 的中型断点样式 */
  @media (min-width: 800px) {
    font-size: 2.6rem;
  }

  /* h1 的大型断点样式 */
  @media (min-width: 1200px) {
    font-size: 3rem;
  }
}

与上述样式等效的非嵌套版本如下:

h1 {
  font-family: var(--font-heading);
  font-size: 2.2rem;
}
@media (min-width: 800px) {
  h1 {
    font-size: 2.6rem;
  }
}

@media (min-width: 1200px) {
  h1 {
    font-size: 3rem;
  }
}

与媒体查询规则类似,也可以对 @layer@supports 等规则启用嵌套写法,如代码清单 8.18 所示。这些规则均对父级选择器 input 生效:

代码清单 8.18 嵌套写法在 @layer 与 @supports 规则中的应用示例

input {
  @layer base { /* 嵌套图层写法下的 input 样式规则 */
    font: inherit;
    border: 2px solid #999;
  }

  @layer modules { /* 嵌套图层写法下的 input 样式规则 */
    padding: 5px 10px;

    /* 仅在支持 :user-invalid 伪类的浏览器中生效的 input 样式 */
    @supports selector(:user-invalid) {
      border-color: green;

      &:user-invalid {
        border-color: red;
      }
    }
  }
}

与代码清单 8.17 一样,上述代码演示了不同 @规则 下的样式嵌套写法,但没有声明任何子选择器(child selector)。类似地,这些样式声明也都将对所在的父级选择器生效。

嵌套写法对于另外两个 @规则 —— @scope@container 同样适用,具体用法将在后续章节详细介绍。

8.4.3.1 始终先写非嵌套声明

Always place nonnested declarations before nested blocks

在本节给出的所有示例中,我都将不带嵌套的样式声明放在了嵌套代码块的开头。强烈建议您也这样做,主要原因有两个。

一是浏览器就是根据这个顺序解析样式的。如果将非嵌套的样式声明放到嵌套代码块的后面,浏览器在实际确定层叠规则的源码顺序时会 逆序执行(reverse the order)。

例如,在代码清单 8.19 中,貌似非嵌套的 2.2rem 字号会覆盖嵌套媒体查询中的字号 2.6rem;然而事实并非如此。当媒体查询条件匹配成功时,在层叠规则中胜出的其实是 2.6rem 字号,感觉就像非嵌套声明(即 2.2rem)原本就写在媒体查询前面一样。

代码清单 8.19 实际位于嵌套代码块前的非嵌套声明示例代码

h1 {
  @media (min-width: 800px) {
    font-size: 2.6rem;
  }

  font-size: 2.2rem; /* 浏览器会将该声明移至 @media 代码块上方,从而颠倒源代码顺序 */
}

如果您用过 CSS 预处理工具,就会知道上述代码与预处理工具中的嵌套行为是一致的。

先写非嵌套声明的第二个原因在于,这样做可以让不支持嵌套写法的浏览器先解析这些普通声明。老版本的浏览器在遇到无法解释的嵌套选择器前,可以先解析诸如 h1 { font-size: 2.2rem; 这样的样式声明,并使之生效。这样往往可以更好地实现优雅降级,而不是让页面在老版本浏览器中几乎没有预定样式。

8.5 本章小结 Summary


  1. 本书使用【层叠图层】的译法,而非网上常见的“级联层”,一来是为了和 CSS 的 C 的译法保持一致;二来是通过 层叠 来强调其对 CSS 层叠行为的干预和控制。对于 MDN 中文版文档译为“级联层”的做法,我本人持保留意见。 ↩︎