别再与层叠样式表作斗争,控制它!

Avatar of Mads Stoumann
Mads Stoumann

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元免费信用!

如果您有纪律并利用层叠样式表提供的继承功能,您最终将编写更少的 CSS。但是,由于我们的样式通常来自各种来源——并且可能难以构建和维护——层叠样式表可能成为挫折的来源,也是我们最终编写更多必要 CSS 的原因。

几年前,Harry Roberts 提出了一种名为 ITCSS 的方法,它是一种构建 CSS 的巧妙方法。

BEM 结合使用,ITCSS 已成为人们编写和组织 CSS 的一种流行方式。

然而,即使使用 ITCSS 和 BEM,我们仍然会在某些情况下仍然与层叠样式表作斗争。例如,我相信您一定遇到过在特定位置@import 外部 CSS 组件以防止破坏某些东西的情况,或者在某个时刻使用可怕的!important

最近,我们的 CSS 工具箱中添加了一些新工具,它们使我们终于可以控制层叠样式表。让我们来看看它们。

哦,层叠样式表,:where你在哪里?

使用 :where 伪选择器 使我们能够将特异性移除到“紧接在用户代理默认样式之后”,无论 CSS 在文档中加载的位置或时间。这意味着整个内容的特异性实际上为零——完全被清除。这对于通用组件非常有用,我们将在稍后进行介绍。

首先,想象一下使用:where 的一些通用<table> 样式

:where(table) {
  background-color: tan;
}

现在,如果您在:where 选择器之前添加一些其他表格样式,如下所示

table {
  background-color: hotpink;
}

:where(table) {
  background-color: tan;
}

…即使在层叠样式表中,table 选择器在:where 选择器之前指定,表格背景也会变为hotpink。这就是:where 的妙处,也是它已经被用于 CSS 重置 的原因。

:where 有一个兄弟选择器,它具有几乎完全相反的效果::is 选择器

:is() 伪类的特异性将被其最特异性的参数的特异性所取代。因此,使用:is() 编写的选择器并不一定具有与不使用:is() 编写的等效选择器相同特异性。 选择器级别 4 规范

扩展我们之前的示例

:is(table) {
  --tbl-bgc: orange;
}
table {
  --tbl-bgc: tan;
}
:where(table) {
  --tbl-bgc: hotpink;
  background-color: var(--tbl-bgc);
}

<table class="c-tbl"> 的背景颜色将是tan,因为:is 的特异性与table 相同,但table 的位置在后面。

但是,如果我们将其更改为以下内容

:is(table, .c-tbl) {
  --tbl-bgc: orange;
}

…背景颜色将是orange,因为:is 具有其最重选择器的权重,即.c-tbl

示例:可配置表格组件

现在,让我们看看如何在组件中使用:where。我们将构建一个表格组件,从 HTML 开始

让我们将.c-tbl 包裹在:where 选择器中,为了好玩,我们还将在表格中添加圆角。这意味着我们需要border-collapse: separate,因为当表格使用border-collapse: collapse 时,我们无法在表格单元格上使用border-radius

:where(.c-tbl) {
  border-collapse: separate;
  border-spacing: 0;
  table-layout: auto;
  width: 99.9%;
}

单元格对<thead><tbody> 单元格使用不同的样式

:where(.c-tbl thead th) {
  background-color: hsl(200, 60%, 40%);
  border-style: solid;
  border-block-start-width: 0;
  border-inline-end-width: 1px;
  border-block-end-width: 0;
  border-inline-start-width: 0;
  color: hsl(200, 60%, 99%);
  padding-block: 1.25ch;
  padding-inline: 2ch;
  text-transform: uppercase;
}
:where(.c-tbl tbody td) {
  background-color: #FFF;
  border-color: hsl(200, 60%, 80%);
  border-style: solid;
  border-block-start-width: 0;
  border-inline-end-width: 1px;
  border-block-end-width: 1px;
  border-inline-start-width: 0;
  padding-block: 1.25ch;
  padding-inline: 2ch;
}

而且,由于我们的圆角和缺少border-collapse: collapse,我们需要添加一些额外的样式,特别是针对表格边框和单元格上的悬停状态

:where(.c-tbl tr td:first-of-type) {
  border-inline-start-width: 1px;
}
:where(.c-tbl tr th:last-of-type) {
  border-inline-color: hsl(200, 60%, 40%);
}
:where(.c-tbl tr th:first-of-type) {
  border-inline-start-color: hsl(200, 60%, 40%);
}
:where(.c-tbl thead th:first-of-type) {
  border-start-start-radius: 0.5rem;
}
:where(.c-tbl thead th:last-of-type) {
  border-start-end-radius: 0.5rem;
}
:where(.c-tbl tbody tr:last-of-type td:first-of-type) {
  border-end-start-radius: 0.5rem;
}
:where(.c-tbl tr:last-of-type td:last-of-type) {
  border-end-end-radius: 0.5rem;
}
/* hover */
@media (hover: hover) {
  :where(.c-tbl) tr:hover td {
    background-color: hsl(200, 60%, 95%);
  }
}

现在,我们可以通过在后注入其他样式来创建表格组件的变体(由:where 的特异性清除功能提供),无论是覆盖.c-tbl 元素,还是添加 BEM 样式的修饰符类(例如c-tbl--purple

<table class="c-tbl c-tbl--purple">
.c-tbl--purple th {
  background-color: hsl(330, 50%, 40%)
}
.c-tbl--purple td {
  border-color: hsl(330, 40%, 80%);
}
.c-tbl--purple tr th:last-of-type {
  border-inline-color: hsl(330, 50%, 40%);
}
.c-tbl--purple tr th:first-of-type {
  border-inline-start-color: hsl(330, 50%, 40%);
}

酷!但是请注意我们如何不断重复颜色?如果我们想更改border-radiusborder-width 会怎样?这会导致大量重复的 CSS。

让我们将所有这些内容移动到 CSS 自定义属性 中,同时,我们可以将所有可配置属性移动到组件“范围”的顶部——即表格元素本身——以便我们以后可以轻松地进行操作。

CSS 自定义属性

我将在 HTML 中更改一些内容,并在表格元素上使用data-component 属性,该属性可以作为样式的目标。

<table data-component="table" id="table">

data-component 将包含我们可以用于任何组件实例的通用样式,即无论我们应用什么颜色变体,表格所需的样式。特定表格组件实例的样式将包含在使用通用组件中的自定义属性的常规类中。

[data-component="table"] {
  /* Styles needed for all table variations */
}
.c-tbl--purple {
  /* Styles for the purple variation */
}

如果我们将所有通用样式放在 数据属性 中,我们可以使用我们想要的任何命名约定。这样,我们就不必担心您的老板是否坚持将表格的类命名为.BIGCORP__TABLE.table-component 或其他名称。

在通用组件中,每个 CSS 属性都指向一个自定义属性。必须在子元素上工作的属性(例如border-color)在通用组件的处指定

:where([data-component="table"]) {
  /* These will will be used multiple times, and in other selectors */
  --tbl-hue: 200;
  --tbl-sat: 50%;
  --tbl-bdc: hsl(var(--tbl-hue), var(--tbl-sat), 80%);
}

/* Here, it's used on a child-node: */
:where([data-component="table"] td) {
  border-color: var(--tbl-bdc);
}

对于其他属性,请决定它应该具有静态值还是使用其自己的自定义属性进行配置。如果您使用自定义属性,请记住定义一个默认值,以便在缺少变体类的情况下表格可以回退到该值。

:where([data-component="table"]) {
  /* These are optional, with fallbacks */
  background-color: var(--tbl-bgc, transparent);
  border-collapse: var(--tbl-bdcl, separate);
}

如果您想知道我如何命名自定义属性,我使用组件前缀(例如--tbl)后跟一个 Emmett 缩写(例如-bgc)。在本例中,--tbl 是组件前缀,-bgc 是背景颜色,-bdcl 是边框折叠。因此,例如,--tbl-bgc 是表格组件的背景颜色。我只在处理组件属性时使用此命名约定,而不是全局属性,全局属性我倾向于保持更通用。

现在,如果我们打开 DevTools,就可以随意操作自定义属性。例如,我们可以将--tbl-hue 更改为 HSL 颜色中的另一个色调值,将--tbl-bdrs: 0 设置为移除border-radius,等等。

A :where CSS rule set showing the custom properties of the table showing how the cascade’s specificity scan be used in context.

在处理您自己的组件时,此时您将发现组件需要哪些参数(即自定义属性值)才能使外观完美无瑕。

我们还可以使用自定义属性来控制列对齐和宽度

:where[data-component="table"] tr > *:nth-of-type(1)) {
  text-align: var(--ca1, initial);
  width: var(--cw1, initial);
  /* repeat for column 2 and 3, or use a SCSS-loop ... */
}

在 DevTools 中,选择表格并将这些属性添加到element.styles 选择器

element.style {
  --ca2: center; /* Align second column center */
  --ca3: right; /* Align third column right */
}

现在,让我们使用常规类.c-tbl(在 BEM 语法中代表“组件表格”)创建特定组件样式。让我们将该类放入表格标记中。

<table class="c-tbl" data-component="table" id="table">

现在,让我们更改 CSS 中的--tbl-hue 值,看看它如何工作,然后再开始乱搞所有属性值

.c-tbl {
  --tbl-hue: 330;
}

请注意,我们只需要更新属性,而不是编写全新的 CSS!更改一个小的属性就会更新表格的颜色——无需使用新的类或覆盖层叠样式表中更低级别的属性。

请注意边框颜色也发生了变化。这是因为表格中的所有颜色都继承自--tbl-hue 变量

我们可以编写更复杂的选择器,但仍然只更新一个属性,从而获得类似于斑马线的效果

.c-tbl tr:nth-child(even) td {
  --tbl-td-bgc: hsl(var(--tbl-hue), var(--tbl-sat), 95%);
}

请记住:加载类的位置无关紧要。因为我们的通用样式使用的是:where,所以特异性会被清除,无论使用特定变体的自定义样式在哪里使用,它们都会被应用。这就是使用:where 来控制层叠样式表的妙处!

最棒的是,我们可以使用几行 CSS 从通用样式中创建各种表格组件。

带有斑马线列的紫色表格
带有“noinlineborder”参数的浅色表格……我们将在下一节中介绍

使用另一个数据属性添加参数

到目前为止,一切都很好!通用表格组件非常简单。但是如果它需要更类似于真实参数的东西呢?也许对于以下情况:

  • 斑马线行和列
  • 粘性页眉和粘性列
  • 悬停状态选项,例如悬停行、悬停单元格、悬停列

我们可以简单地添加 BEM 样式的修饰符类,但实际上我们可以通过添加另一个 data-attribute 来更高效地实现它。也许一个包含参数的 data-param 就像这样

<table data-component="table" data-param="zebrarow stickyrow">

然后,在我们的 CSS 中,我们可以使用 属性选择器 来匹配参数列表中的完整单词。例如,斑马纹行

[data-component="table"][data-param~="zebrarow"] tr:nth-child(even) td {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
}

或者斑马纹列

[data-component="table"][data-param~="zebracol"] td:nth-of-type(odd) {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
}

让我们疯狂一点,让表格标题和第一列都固定


[data-component="table"][data-param~="stickycol"] thead tr th:first-child,[data-component="table"][data-param~="stickycol"] tbody tr td:first-child {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
  inset-inline-start: 0;
  position: sticky;
}
[data-component="table"][data-param~="stickyrow"] thead th {
  inset-block-start: -1px;
  position: sticky;
}

这是一个允许您一次更改一个参数的演示

演示中的默认浅色主题是

.c-tbl--light {
  --tbl-bdrs: 0;
  --tbl-sat: 15%;
  --tbl-th-bgc: #eee;
  --tbl-th-bdc: #eee;
  --tbl-th-c: #555;
  --tbl-th-tt: normal;
}

…其中 data-param 设置为 noinlineborder,对应于以下样式

[data-param~="noinlineborder"] thead tr > th {
  border-block-start-width: 0;
  border-inline-end-width: 0;
  border-block-end-width: var(--tbl-bdw);
  border-inline-start-width: 0;
}

我知道我的 data-attribute 样式和配置通用组件的方式非常主观。这只是我的做法,所以请随意坚持使用您最习惯使用的方法,无论是 BEM 修饰符类还是其他方法。

最重要的是:拥抱 :where:is 以及它们提供的级联控制能力。 并且,如果可能,以这样一种方式构建 CSS,这样您在创建新的组件变体时只需编写尽可能少的新的 CSS 代码!

级联层

我想看的最后一个级联破坏工具是“级联层”。在撰写本文时,它是 CSS 级联和继承级别 5 规范 中定义的一个实验性功能,您可以在 Safari 或 Chrome 中通过启用 #enable-cascade-layers 标志来访问它。

Bramus Van Damme 很好地总结了这个概念

级联层的真正力量来自它在级联中的独特位置:在选择器特异性之前和出现顺序之前。因此,我们无需担心其他层中使用的 CSS 的选择器特异性,也不必担心我们将 CSS 加载到这些层的顺序 - 这对于大型团队或加载第三方 CSS 时非常有用。

也许更棒的是他用图示展示了级联层在级联中的位置

致谢:Bramus Van Damme

在本文的开头,我提到了 ITCSS - 一种通过指定通用样式、组件等的加载顺序来驯服级联的方法。级联层允许我们在给定位置注入样式表。因此,此结构在级联层中的简化版本如下所示

@layer generic, components;

有了这一行代码,我们就决定了层的顺序。首先是通用样式,然后是组件特定的样式。

假设我们正在加载我们的通用样式,而这些样式远远晚于我们的组件样式

@layer components {
  body {
    background-color: lightseagreen;
  }
}

/* MUCH, much later... */

@layer generic { 
  body {
    background-color: tomato;
  }
}

background-color 将是 lightseagreen,因为我们的组件样式层设置在通用样式层之后。因此,即使 components 层中的样式 generic 层样式之前编写,它们也会“获胜”。

同样,这只是控制 CSS 级联如何应用样式的另一个工具,它使我们能够更灵活地以逻辑方式组织内容,而不是与特异性作斗争。

现在您掌握了控制权!

这里的重点是,由于新功能的出现,CSS 级联变得更容易驾驭。我们看到 :where:is 伪选择器如何让我们控制特异性,无论是通过去除整个规则集的特异性,还是分别采用最具体参数的特异性。然后,我们使用 CSS 自定义属性来覆盖样式,而无需编写一个新的类来覆盖另一个。从那里,我们稍稍绕道进入 data-attribute 路线,以帮助我们通过向 HTML 添加参数来更灵活地创建组件变体。最后,我们研究了级联层,它应该有助于使用 @layer 指定样式的加载顺序。

如果您从本文中只带走了一点,我希望它是,CSS 级联不再是人们常说的那个敌人。我们正在获得工具来停止与它作斗争,并开始更多地利用它。


标题照片来自 Stephen Leonardi 在 Unsplash