级联层指南

Avatar of Miriam Suzanne
Miriam Suzanne 发表于

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

这是您关于 CSS 级联层的完整指南,这是一个 CSS 功能,允许我们定义明确包含的特异性层,以便我们完全控制哪些样式在项目中优先,而无需依赖特异性技巧或 !important。本指南旨在帮助您全面了解级联层的用途、如何以及为何选择使用它们、当前的支持级别以及如何使用它们的语法。

目录

快速示例

/* establish a layer order up-front, from lowest to highest priority */
@layer reset, defaults, patterns, components, utilities, overrides;

/* import stylesheets into a layer (dot syntax represents nesting) */
@import url('framework.css') layer(components.framework);

/* add styles to layers */
@layer utilities {
  /* high layer priority, despite low specificity */
  [data-color='brand'] { 
    color: var(--brand, rebeccapurple);
  }
}

@layer defaults {
  /* higher specificity, but lower layer priority */
  a:any-link { color: maroon; }
}

/* un-layered styles have the highest priority */
a {
  color: mediumvioletred;
}

简介:什么是级联层?

CSS 级联层旨在解决 CSS 中的棘手问题。让我们来看看**主要问题**以及级联层如何解决它。

问题:特异性冲突升级

我们中的许多人都遇到过这样的情况:由于选择器冲突,我们希望覆盖代码中其他地方(或第三方工具)的样式。多年来,开发者们已经开发了许多“方法”和“最佳实践”来避免这些情况——例如“所有选择器只使用一个类”。这些规则通常更多的是关于避免级联,而不是利用它

管理级联冲突和选择器特异性通常被认为是 CSS 中比较困难——或者至少是比较令人困惑——的方面之一。这可能部分是因为很少有其他语言依赖级联作为其核心功能,但也确实如此,原始级联严重依赖于*启发式*(内置于代码中的有根据的猜测或假设),而不是为 Web 作者提供直接和明确的控制。

例如,选择器特异性——我们与级联的主要交互方式——是基于这样的假设:更窄目标的样式(例如仅使用一次的 ID)可能比更通用和可重用样式(例如类和属性)更重要。也就是说:选择器有多*具体*。这是一个很好的猜测,但它不是一个完全可靠的规则,这会导致一些问题

  • 它将*选择元素*的行为与*优先级规则集*的行为结合起来。
  • “解决”特异性冲突的最简单方法是通过添加其他不必要的选择器来升级问题,或者(喘气)扔出 !important 手榴弹
.overly#powerful .framework.widget {
  color: maroon;
}

.my-single_class { /* add some IDs to this ??? */
  color: rebeccapurple; /* add !important ??? */
}

解决方案:级联层提供控制

级联层 使 CSS 作者能够更直接地控制级联,因此我们可以构建更有意图的级联系统,而无需过多依赖与选择相关的启发式假设。

使用 @layer at 规则和分层 @import,我们可以建立我们自己的*级联层*——从低优先级样式(如重置和默认值)开始,到主题、框架和设计系统,再到最高优先级样式,如组件、实用程序和覆盖。特异性仍然适用于*每个层内*的冲突,但层之间的冲突始终通过使用更高优先级的层样式来解决。

@layer framework {
  .overly#powerful .framework.widget {
    color: maroon;
  }
}

@layer site {
  .my-single_class {
    color: rebeccapurple;
  }
}

这些层是有序和分组的,因此它们不会像特异性和重要性那样升级。级联层不像选择器那样累积。添加*更多层*不会使某些东西*更重要*。它们也不像重要性那样是二元的——突然跳到堆栈的顶部——或者像 z-index 那样是编号的,我们必须猜测一个很大的数字(z-index: 9999999?)。事实上,默认情况下,分层样式比非分层样式*更不重要*。

@layer defaults {
  a:any-link { color: maroon; }
}

/* un-layered styles have the highest priority */
a {
  color: mediumvioletred;
}

层在级联中处于什么位置?

级联是一系列步骤(算法),用于解决样式之间的冲突。

html { --button: teal; }
button { background: rebeccapurple !important; }
.warning { background: maroon; }
<button class="warning" style="background: var(--button);">
  what color background?
</button>

添加级联层后,这些步骤是

Illustration of the various specificity levels of the CSS cascade and where CSS Cascade Layers fit in it.

*选择器特异性*只是级联的一小部分,但它也是我们互动最多的步骤,并且通常用于更普遍地指代整体*级联优先级*。人们可能会说 !important 标记或 style 属性“增加了特异性”——这是一种表达样式在级联中变得更高优先级的快速方法。由于级联层已直接添加到特异性之上,因此以类似的方式考虑它们是合理的:比 ID 选择器更强大的一步。

但是,CSS 级联层也使我们必须完全理解 !important 在级联中的作用——不仅仅是作为“增加特异性”的工具,而是作为平衡关注点的系统。

!important 的起源、上下文和层是反向的!

作为 Web 作者,我们通常认为 !important 是一种增加特异性的方法,可以覆盖内联样式或高度特定的选择器。这在大多数情况下都适用(如果您对升级没问题),但它忽略了*重要性*作为整体级联中一项功能的主要目的。

重要性不仅仅是为了简单地增加权力——而是为了平衡各种竞争关注点之间的权力。

重要性起源

这一切都始于起源,即样式在 Web 生态系统中的来源。CSS 中有三个基本起源

  • **浏览器**(或用户代理)
  • **用户**(通常通过浏览器首选项)
  • Web **作者**(那就是我们!)

浏览器为所有元素提供可读的默认值,然后用户设置他们的首选项,然后我们(作者)为我们的网页提供预期的设计。因此,默认情况下,浏览器具有最低优先级,用户首选项会覆盖浏览器默认值,并且我们能够覆盖所有人。

但是 CSS 的创建者非常清楚我们不应该拥有最终决定权

如果发生冲突,**用户应该拥有最终决定权**,但也应该允许作者附加样式提示。

—— Håkon Lie(强调补充)

因此,*重要性*为浏览器和用户提供了一种在最重要的时候重新获得控制权的方法。当 !important 标记添加到样式中时,会创建三个新层——并且顺序相反!

  1. !important 浏览器样式(最强大)
  2. !important 用户首选项
  3. !important 作者样式
  4. 普通作者样式
  5. 普通用户首选项
  6. 普通浏览器样式(最不强大)

对我们来说,添加 !important 并没有太大改变——我们的重要样式与我们的普通样式非常接近——但对于浏览器和用户来说,这是一个非常强大的重新获得控制权的工具。浏览器默认样式表包含许多我们无法覆盖的重要样式,例如

iframe:fullscreen {
  /* iframes in full-screen mode don't show a border. */
  border: none !important;
  padding: unset !important;
}

虽然大多数流行的浏览器都难以上传实际的用户样式表,但它们都提供用户首选项:用于建立特定用户样式的图形界面。在该界面中,始终有一个复选框可供用户选择是否允许网站覆盖其首选项。这与在用户样式表中设置 !important 相同

Screenshot of user font preferences.

重要的上下文

同样的基本逻辑也适用于层叠中的*上下文*。默认情况下,来自宿主文档(light DOM)的样式会覆盖来自嵌入上下文(shadow DOM)的样式。但是,添加 !important 会颠倒顺序

  1. !important shadow 上下文(最强)
  2. !important host 上下文
  3. 普通 host 上下文
  4. 普通 shadow 上下文(最弱)

来自 shadow 上下文内部的重要样式会覆盖宿主文档定义的重要样式。这是一个 odd-bird 自定义元素,其中一些样式写在元素模板(shadow DOM)中,一些样式写在宿主页面(light DOM)样式表中

两个 color 声明都具有正常的权重,因此宿主页面的 mediumvioletred 优先级更高。但是 font-family 声明被标记为 !important,这使得 shadow 上下文(其中定义了 fantasy)具有优势。

重要的层

层叠层的工作方式与来源和上下文相同,重要的层以相反的顺序排列。唯一的区别是层使这种行为更加明显。

一旦我们开始使用层叠层,我们就需要更加谨慎和有意地使用 !important。它不再是快速跳到优先级顶部的方法,而是层叠分层的一个组成部分;一种让较低层*坚持*其某些样式必不可少的方法。

由于层叠层是可定制的,因此没有预定义的顺序。但是我们可以想象从三个层开始

  1. 实用程序(最强)
  2. 组件
  3. 默认值(最弱)

当这些层中的样式被标记为重要时,它们将生成三个新的、反向的重要层

  1. !important 默认值(最强)
  2. !important 组件
  3. !important 实用程序
  4. 普通实用程序
  5. 普通组件
  6. 普通默认值(最弱)

在这个例子中,颜色是由所有三个普通层定义的,并且 utilities 层赢得了冲突,应用了 maroon 颜色,因为 utilities 层在 @layers 中具有更高的优先级。但是请注意,text-decoration 属性在 defaultscomponents 层中都被标记为 !important,其中重要的 defaults 优先,应用了 defaults 声明的下划线

建立层顺序

我们可以创建任意数量的层,并以各种方式命名或分组它们。但最重要的是要确保我们的层按正确的优先级顺序应用。

单个层可以在整个代码库中多次使用 - 层叠层按它们*首次出现*的顺序堆叠。遇到的第一个层位于底部(最弱),最后一个层位于顶部(最强)。但是,在此之上,**未分层样式具有最高优先级**

@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 { a { color: yellow; } }
/* un-layered */ a { color: green; }
  1. 未分层样式(最强)
  2. layer-3
  3. layer-2
  4. layer-1(最弱)

然后,如上所述,任何重要的样式都以相反的顺序应用

@layer layer-1 { a { color: red !important; } }
@layer layer-2 { a { color: orange !important; } }
@layer layer-3 { a { color: yellow !important; } }
/* un-layered */ a { color: green !important; }
  1. !important layer-1(最强)
  2. !important layer-2
  3. !important layer-3
  4. !important 未分层样式
  5. 普通未分层样式
  6. 普通 layer-3
  7. 普通 layer-2
  8. 普通 layer-1(最弱)

层也可以分组,允许我们对顶级层和嵌套层进行更复杂的排序

@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 {
  @layer sub-layer-1 { a { color: yellow; } }
  @layer sub-layer-2 { a { color: green; } }
  /* un-nested */ a { color: blue; }
}
/* un-layered */ a { color: indigo; }
  1. 未分层样式(最强)
  2. layer-3
    1. layer-3 未嵌套
    2. layer-3 sub-layer-2
    3. layer-3 sub-layer-1
  3. layer-2
  4. layer-1(最弱)

分组层始终保持在最终层顺序中(例如,layer-3 的子层将全部彼此相邻),但这与将列表“扁平化”相同 - 将其转换为单个六项列表。反转 !important 层顺序时,整个扁平化列表都会反转。

但是层不必在单个位置定义一次。我们给它们命名,以便可以在一个地方定义层(以建立层顺序),然后我们可以从任何地方向它们追加样式

/* describe the layer in one place */
@layer my-layer;

/* append styles to it from anywhere */
@layer my-layer { a { color: red; } }

我们甚至可以在单个声明中定义整个有序的层列表

@layer one, two, three, four, five, etc;

这使得网站的作者可以对层顺序拥有最终决定权。通过在导入任何第三方代码之前预先提供层顺序,可以在一个地方建立和重新排列顺序,而无需担心在任何第三方工具中如何使用层。

语法:使用层叠层

让我们看看语法!

顺序设置 @layer 语句

由于层按定义的顺序堆叠,因此我们有一个工具可以在一个地方建立该顺序非常重要!

我们可以使用 @layer 语句来做到这一点。语法是

@layer <layer-name>#;

该井号 (#) 意味着我们可以在逗号分隔的列表中添加任意数量的层名称

@layer reset, defaults, framework, components, utilities;

这将建立层顺序

  1. 未分层样式(最强)
  2. utilities
  3. 组件
  4. framework
  5. defaults
  6. reset(最弱)

我们可以这样做任意次数,但请记住:重要的是每个名称*首次出现*的顺序。所以这将具有相同的结果

@layer reset, defaults, framework;
@layer components, defaults, framework, reset, utilities;

排序逻辑将忽略第二个 @layer 规则中 resetdefaultsframework 的顺序,因为这些层已经建立。此 @layer 列表语法不会为层排序逻辑添加任何特殊魔法:层是根据*它们在代码中首次出现的顺序*堆叠的。在这种情况下,reset 首次出现在第一个 @layer 列表中。任何后面的 @layer 语句只能将层名称追加到列表中,*但不能移动已经存在的层*。这确保您始终可以从一个位置(样式的最开始)控制最终的整体层顺序。

这些层排序语句允许在样式表的顶部,在 @import 规则之前(但不能在导入之间)。我们强烈建议使用此功能预先在一个地方建立所有层,以便您始终知道在哪里查找或进行更改。

@layer 规则

@layer 规则的块版本仅采用单个层名称,但随后允许您向该层添加样式

@layer <layer-name> {
  /* styles added to the layer */
}

您可以将大多数内容放在 @layer 块中——媒体查询、选择器和样式、支持查询等。您不能放在层块中的唯一内容是字符集、导入和命名空间之类的内容。但别担心,有一种将样式导入到层的语法。

如果之前尚未建立层名称,则此层规则会将其添加到层顺序中。但是,如果已建立名称,则允许您从文档中的任何位置向现有层添加样式,而无需更改每个层的优先级。

如果我们已经使用 layer 语句规则预先建立了层顺序,我们不再需要担心这些层块的顺序

/* establish the order up-front */
@layer defaults, components, utilities;

/* add styles to layers in any order */
@layer utilities {
  [hidden] { display: none; }
}

/* utilities will override defaults, based on established order */
@layer defaults {
  * { box-sizing: border-box; }
  img { display: block; }
}

分组(嵌套)层

可以通过嵌套层规则对层进行分组

@layer one {
  /* sorting the sub-layers */
  @layer two, three;

  /* styles ... */
  @layer three { /* styles ... */ }
  @layer two { /* styles ... */ }
}

这会生成可以通过将父名称和子名称与句点连接来表示的分组层。这意味着也可以从组外部直接访问生成的子层

/* sorting nested layers directly */
@layer one.two, one.three;

/* adding to nested layers directly */
@layer one.three { /* ... */ }
@layer one.two { /* ... */ }

层排序规则适用于每一级嵌套。任何未进一步嵌套的样式在该上下文中被视为“未分层”,并且优先于进一步嵌套的样式

@layer defaults {
  /* un-layered defaults (higher priority) */
  :any-link { color: rebeccapurple; }

  /* layered defaults (lower priority) */
  @layer reset {
    a[href] { color: blue; }
  }
}

分组层也包含在其父层中,因此层顺序不会跨组混合。在此示例中,首先对顶级层进行排序,然后在每个组内对层进行排序

@layer reset.type, default.type, reset.media, default.media;

导致层顺序为

  • *未分层*(最强)
  • default 组
    • default *未分层*
    • default.media
    • default.type
  • reset 组
    • reset *未分层*
    • reset.media
    • reset.type

请注意,层名称也是作用域化的,因此它们不会与其嵌套上下文之外的同名层交互或冲突。两个组都可以具有不同的 media 子层。

当使用 @import<link> 来分层整个样式表时,这种分组变得尤为重要。像 Bootstrap 这样的第三方工具,可以在内部使用层级——但我们可以在导入时将这些层级嵌套到一个共享的 bootstrap 层组中,以避免潜在的层级命名冲突。

可以使用带有 @import 规则的新 layer() 函数语法将整个样式表添加到层中

/* styles imported into to the <layer-name> layer */
@import url('example.css') layer(<layer-name>);

还有一个提案建议在 HTML <link> 元素中添加 layer 属性——尽管这仍在开发中,并且 尚无任何地方支持。这可以用于导入第三方工具或组件库,同时将所有内部层级分组到单个层级名称下——或者作为将层级组织到不同文件的一种方式。

匿名(未命名)层

层名称 很有用,因为它们允许我们从多个位置访问同一个层进行排序或组合层块——但它们不是必需的。

可以使用块层规则创建_匿名_(未命名)层

@layer { /* ... */ }
@layer { /* ... */ }

或者使用导入语法,用 layer 关键字代替 layer() 函数

/* styles imported into to a new anonymous layer */
@import url('../example.css') layer;

每个匿名层都是唯一的,并添加到遇到它的层顺序中。匿名层不能从其他层规则中引用以进行排序或追加更多样式。

这些应该谨慎使用,但可能有一些用例

  • 项目可以确保给定层的所有样式都必须位于单个位置。
  • 第三方工具可以将它们的内部层级“隐藏”在匿名层中,这样它们就不会成为该工具公共 API 的一部分。

将值还原到上一层

我们可以使用几种方法将级联中的样式“还原”到由较低优先级来源或层定义的先前值。这包括许多现有的全局 CSS 值,以及一个新的 revert-layer 关键字,该关键字也将是全局的(适用于任何属性)。

上下文:现有的全局级联关键字*

CSS 有几个 全局关键字,可以用于任何属性,以帮助以各种方式回滚级联。

  • initial 将属性设置为应用任何样式(包括浏览器默认值)之前的_指定_值。这可能令人惊讶,因为我们通常将浏览器样式视为初始值——但是,例如,display 的初始值是 inline,无论我们将其用于哪个元素。
  • inherit 将属性设置为应用其父元素的值。这是继承属性的默认值,但仍然可以用来删除以前的值。
  • unset 的作用就像简单地删除所有先前的值一样——这样继承的属性再次 inherit,而非继承的属性返回到它们的 initial 值。
  • revert 只删除我们在作者来源(即站点样式)中应用的值。这在大多数情况下是我们想要的,因为它允许浏览器和用户样式保持不变。
新增:revert-layer 关键字

级联层添加了一个新的全局 revert-layer 关键字。它的工作原理与 revert 相同,但只删除我们在当前级联层中应用的值。我们可以使用它来回滚级联,并使用在先前层中定义的任何值。

在这个例子中,no-theme 类删除了在 theme 层中设置的任何值。

@layer default {
  a { color: maroon; }
}

@layer theme {
  a { color: var(--brand-primary, purple); }

  .no-theme {
    color: revert-layer;
  }
}

因此,带有 .no-theme 类的链接标签将回滚到使用在 default 层中设置的值。当 revert-layer 用于非分层样式时,它的行为与 revert 相同——回滚到先前的来源。

还原重要层

如果我们将 !important 添加到 revert-layer 关键字,事情就会变得有趣。因为每个层都有两个不同的“正常”和“重要” 级联位置,这不仅仅是改变声明的优先级——它改变了哪些层被还原。

假设我们定义了三个层,它们在层堆栈中如下所示

  1. 实用程序(最强)
  2. 组件
  3. 默认值(最弱)

我们可以将其充实,不仅包括每个层的正常和重要位置,还包括非分层样式和动画

  1. !important 默认值(最强)
  2. !important 组件
  3. !important 实用程序
  4. !important 未分层样式
  5. CSS 动画
  6. 普通未分层样式
  7. 普通实用程序
  8. 普通组件
  9. 普通默认值(最弱)

现在,当我们在正常层中使用 revert-layer 时(让我们使用 utilities),结果相当直接。我们_只还原该层_,而其他所有内容都正常应用

  1. !important 默认值(最强大)
  2. !important 组件
  3. !important 实用程序
  4. !important 非分层样式
  5. ✅ CSS 动画
  6. ✅ 正常非分层样式
  7. ❌ 正常实用程序
  8. ✅ 正常组件
  9. ✅ 正常默认值(最不强大)

但是当我们将 revert-layer 移到重要位置时,我们会还原正常和重要版本_以及介于两者之间的所有内容_

  1. !important 默认值(最强大)
  2. !important 组件
  3. !important 实用程序
  4. !important 非分层样式
  5. ❌ CSS 动画
  6. ❌ 正常非分层样式
  7. ❌ 正常实用程序
  8. ✅ 正常组件
  9. ✅ 正常默认值(最不强大)

用例:我什么时候想要使用级联层?

那么,我们可能会在什么样的情况下使用级联层呢?这里有几个例子,说明级联层在什么时候很有意义,以及在什么时候_没有_什么意义。

侵入性较低的重置和默认值

最初最明显的用例之一是使低优先级默认值易于覆盖。

一些重置已经通过在每个选择器周围应用 :where() 伪类来做到这一点。:where() 从应用它的选择器中删除所有特异性,这具有所需的基本影响,但也有一些缺点

  • 它必须单独应用于每个选择器
  • 重置内部的冲突必须在没有特异性的情况下解决

层级允许我们更简单地包装整个重置样式表,可以使用块 @layer 规则

/* reset.css */
@layer reset {
  /* all reset styles in here */
}

或者当你导入重置时

/* reset.css */
@import url(reset.css) layer(reset);

或者两者兼而有之!层级可以嵌套而不改变其优先级。这样,你可以使用第三方重置,并确保它被添加到你想要的层级中,无论重置样式表本身是否在内部使用层级编写。

由于分层样式的优先级低于默认的“非分层”样式,因此这是在不重写整个 CSS 代码库的情况下开始使用级联层的好方法。

重置选择器仍然具有特异性信息来帮助解决内部冲突,而无需包装每个单独的选择器——但你也可以获得易于覆盖的重置样式表的预期结果。

管理复杂的 CSS 架构

随着项目变得越来越大、越来越复杂,为 CSS 代码的命名和组织定义更清晰的界限可能很有用。但是,我们拥有的 CSS 越多,发生冲突的可能性就越大——尤其是来自系统不同部分的冲突,比如“主题”、“组件库”或一组“实用程序类”。

我们不仅希望这些按功能组织,而且根据系统哪些部分在发生冲突时具有优先级来组织它们也很有用。Harry Roberts 的倒三角 CSS 很好地形象化了这些层可能包含的内容。

事实上,将层添加到 CSS 级联的最初提议使用 ITCSS 方法作为主要示例,以及开发该功能的指南。

这不需要特定的技术,但将项目限制为一组预定义的顶级层,然后根据需要使用嵌套层扩展该集合可能会有所帮助。

例如

  1. 低级重置和规范化样式
  2. 元素默认值,用于基本排版和易读性
  3. 主题,例如亮色和暗色模式
  4. 可能出现在多个组件中的可重用模式
  5. 布局和更大的页面结构
  6. 单个组件
  7. 覆盖实用程序

我们可以在 CSS 的最开始创建顶层堆栈,只需一个层声明

@layer
  reset,
  default,
  themes,
  patterns,
  layouts,
  components,
  utilities;

所需的具体层以及如何命名这些层,可能会因项目而异。

从那里,我们创建更详细的层细分。也许我们的组件本身内部就有默认值、结构、主题和实用程序。

@layer components {
  @layer defaults, structures, themes, utilities;
}

在不改变顶层结构的情况下,我们现在有了一种方法可以进一步对每个组件内的样式进行分层。

使用第三方工具和框架

将第三方 CSS 集成到项目中是最容易遇到层叠问题的场景之一。无论我们使用的是像 Normalizer 或 CSS Remedy 这样的共享重置,还是像 Material Design 这样的通用设计系统,像 Bootstrap 这样的框架,或者像 Tailwind 这样的实用程序工具包,我们都无法始终控制网站上所有使用的 CSS 的选择器特异性和重要性。有时,这甚至扩展到组织中其他地方管理的内部库、设计系统和工具。

因此,我们经常不得不围绕第三方代码构建我们的内部 CSS,或者在冲突出现时使用人为提高的特异性或 !important 标记来升级冲突。然后我们必须随着时间的推移维护这些 hack,以适应上游的变化。

层叠层为我们提供了一种方法,可以将第三方代码插入到任何项目的层叠中的确切位置,无论内部如何编写选择器。根据我们使用的库类型,我们可以通过各种方式来做到这一点。让我们从一个基本的层堆栈开始,从重置到实用程序逐步向上。

@layer reset, type, theme, components, utilities;

然后我们可以整合一些工具…

使用重置

如果我们使用像 CSS Remedy 这样的工具,我们可能还有一些我们想要包含的自己的重置样式。让我们将 CSS Remedy 导入 reset 的子层中

@import url('remedy.css') layer(reset.remedy);

现在我们可以将我们自己的重置样式添加到 reset 层中,而无需任何进一步的嵌套(除非我们想要它)。由于直接在 reset 中的样式将覆盖任何进一步嵌套的样式,因此如果发生冲突,我们可以确保我们的样式始终优先于 CSS Remedy,无论新版本中发生什么变化。

@import url('remedy.css') layer(reset.remedy);

@layer reset {
  :is(ol, ul)[role='list'] {
    list-style: none;
    padding-inline-start: 0;
  }
}

由于 reset 层位于堆栈的底部,因此我们系统中的其余 CSS 将覆盖 Remedy 和我们自己的本地重置添加。

使用实用程序类

在我们堆栈的另一端,CSS 中的“实用程序类”可以是一种以广泛适用的方式复制常见模式(例如为屏幕阅读器添加上下文)的有用方法。实用程序倾向于打破特异性启发式方法,因为我们希望它们被广泛定义(导致低特异性),但我们通常也希望它们“赢得”冲突。

通过在层堆栈的顶部放置一个 utilities 层,我们可以实现这一点。我们可以以类似于重置示例的方式使用它,将外部实用程序加载到子层中,并提供我们自己的实用程序。

@import url('tailwind.css') layer(utilities.tailwind);

@layer utilities {
  /* from https://kittygiraudel.com/snippets/sr-only-class/ */
  /* but with !important removed from the properties */
  .sr-only {
    border: 0;
    clip: rect(1px, 1px, 1px, 1px);
    -webkit-clip-path: inset(50%);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    margin: -1px;
    padding: 0;
    position: absolute;
    width: 1px;
    white-space: nowrap;
  }
}
使用设计系统和组件库

有很多 CSS 工具位于我们层堆栈的中间 - 结合了排版默认值、主题、组件和系统的其他方面。

根据特定的工具,我们可能会做类似于重置和实用程序示例的操作 - 但还有一些其他选项。一个高度集成的工具可能需要一个顶层。

@layer reset, bootstrap, utilities;
@import url('bootstrap.css') layer(bootstrap);

如果这些工具开始将层作为其公共 API 的一部分提供,我们还可以将其分解成多个部分 - 允许我们将我们的代码与库穿插使用。

@import url('bootstrap/reset.css') layer(reset.bootstrap);
@import url('bootstrap/theme.css') layer(theme.bootstrap);
@import url('bootstrap/components.css') layer(components.bootstrap);

@layer theme.local {
  /* styles here will override theme.bootstrap */
  /* but not interfere with styles from components.bootstrap */
}

将层与现有的(未分层、充满 !important 的)框架一起使用

与任何主要的语言变化一样,当 CSS 层叠层被广泛采用时,将会有一个调整期。如果你的团队准备在下个月开始使用层,但你最喜欢的框架决定再等三年才切换到分层样式,会发生什么?许多框架可能仍然会比我们希望的更频繁地使用 !important!随着 !important 层的颠倒,这并不理想。

尽管如此,层仍然可以帮助我们解决问题。我们只需要聪明地利用它。我们决定我们想要为我们的项目设置哪些层,这意味着我们可以在我们创建的框架层之上和之下添加层。

但是现在,我们可以使用较低的层来覆盖框架中的 !important 样式,并使用较高的层来覆盖普通样式。像这样

@layer framework.important, framework.bootstrap, framework.local;
@import url('bootstrap.css') layer(framework.bootstrap);

@layer framework.local {
  /* most of our normal framework overrides can live here */
}

@layer framework.important {
  /* add !important styles in a lower layer */
  /* to override any !important framework styles */
}

它仍然感觉有点像 hack,但它有助于我们朝着正确的方向前进 - 朝着更结构化的层叠前进。希望这是一个临时修复。

设计 CSS 工具或框架

对于维护 CSS 库的任何人来说,层叠层可以帮助进行内部组织,甚至可以成为开发者 API 的一部分。通过命名库的内部层,我们可以允许我们框架的用户在自定义或覆盖我们提供的样式时挂钩到这些层。

例如,Bootstrap 可以为他们的“reboot”、“grid”和“utilities”公开层 - 可能是按此顺序堆叠的。现在用户可以决定是否要将这些 Bootstrap 层加载到不同的本地层中。

@import url(bootstrap/reboot.css) layer(reset); /* reboot » reset.reboot */
@import url(bootstrap/grid.css) layer(layout); /* grid » layout.grid */
@import url(bootstrap/utils.css) layer(override); /* utils » override.utils */

或者用户可以将它们加载到 Bootstrap 层中,并穿插本地层。

@layer bs.reboot, bs.grid, bs.grid-overrides, bs.utils, bs.util-overrides;
@import url('bootstrap-all.css') layer(bs);

如果需要,还可以通过将任何私有/内部层分组在一个匿名(未命名)层中来向用户隐藏内部分层。匿名层将被添加到遇到它们的层顺序中,但不会暴露给重新排列或追加样式的用户。

我只希望这一个属性更 !important

与一些预期相反,层并不能轻松地快速升级特定样式,使其覆盖另一个样式。

如果我们的大多数样式未分层,那么任何新层相对于默认层都将被 _降级_。我们可以对单个样式块执行此操作,但很快就会变得难以跟踪。

层旨在更基础,而不是逐个样式,而是在整个项目中建立一致的模式。理想情况下,如果我们正确设置了它,我们可以通过将我们的样式移动到适当的(和预定义的)层来获得正确的结果。

如果我们的大多数样式已经属于定义明确的层,我们总是可以考虑在给定堆栈的顶部添加一个新的最高功率层,或者使用未分层的样式来覆盖这些层。我们甚至可以考虑在堆栈顶部放置一个 debug 层,以便在生产环境之外进行探索性工作。

但是,动态添加新层可能会破坏此功能的组织实用性,因此应谨慎使用。最好问:_为什么这种样式应该覆盖另一种样式?_

如果答案与一种 _样式类型_ 始终覆盖另一种样式类型有关,那么层可能是正确的解决方案。这可能是因为我们正在覆盖来自我们无法控制的地方的样式,或者因为我们正在编写一个实用程序,并且它应该移动到我们的 utilities 层中。如果答案与更有针对性的样式覆盖针对性较低的样式有关,我们可能会考虑使选择器反映这种特异性。

或者,在极少数情况下,我们甚至可能拥有真正重要的样式——如果您覆盖此特定样式,该功能根本无法正常工作。我们可能会说,将 display: none 添加到 [hidden] 属性属于我们最低优先级的重置,但仍然应该难以覆盖。在这种情况下,!important 确实是完成这项工作的正确工具。

@layer reset {
  [hidden] { display: none !important; }
}

作用域和命名空间样式?不!

层叠层显然是一种组织工具,它可以“捕获”选择器的影响,尤其是在它们冲突时。因此,乍一看,很容易将它们视为管理作用域或命名空间的解决方案。

一个常见的本能反应是为项目中的每个组件创建一个层——希望这将确保(例如).post-title 仅应用于 .post 内部。

但是层叠冲突与命名冲突不同,层也不太适合这种类型的范围组织。层叠层不会限制选择器如何匹配或应用于 HTML,只会限制它们如何级联在一起。因此,除非我们能够确保组件 X 始终 覆盖组件 Y,否则单个组件层不会有太大帮助。相反,我们需要关注正在开发的提议的 @scope 规范

将层和组件作用域视为重叠的关注点可能会有所帮助。

An illustration showing how CSS Cascade Layers can be organized by scope, such as buttons, cards, and login layers that fall into component, theme, and default scopes.

作用域描述了我们正在设置样式的内容,而层描述了我们设置样式的原因。我们还可以将层视为表示样式的来源,而作用域表示样式将附加到的内容

测试你的知识:哪种样式会赢?

对于每种情况,假设此段落

<p id="intro">Hello, World!</p>

问题 1

@layer ultra-high-priority {
  #intro {
    color: red;
  }
}

p {
  color: green;
}
段落是什么颜色?

尽管该层的名称听起来非常重要,但未分层的样式在级联中具有更高的优先级。所以段落将是 green(绿色)。

问题 2

@layer ren, stimpy;

@layer ren {
  p { color: red !important; }
}

p { color: green; }

@layer stimpy {
  p { color: blue !important; }
}
段落是什么颜色?

我们的正常层顺序在开始时就建立了——ren 在底部,然后是 stimpy,然后(一如既往)未分层的样式在顶部。但这些样式并非都是 normal(正常的),其中一些是重要的。我们马上就可以过滤掉只有 !important 的样式,而忽略不重要的 green(绿色)。请记住,“来源和重要性”是级联的第一步,甚至在我们考虑分层之前。

这给我们留下了两种重要的样式,都在层中。由于我们重要的层被颠倒了,ren 移到顶部,stimpy 移到底部。段落将是 red(红色)。

问题 3

@layer Montagues, Capulets, Verona;

@layer Montagues.Romeo { #intro { color: red; } }
@layer Montagues.Benvolio { p { color: orange; } }

@layer Capulets.Juliet { p { color: yellow; } }
@layer Verona { * { color: blue; } }
@layer Capulets.Tybalt { #intro { color: green; } }
段落是什么颜色?

我们所有的样式都在相同的来源和上下文中,没有一个被标记为重要,也没有一个是内联样式。我们这里确实有各种各样的选择器,从高度特定的 ID #intro 到零特异性的通用 * 选择器。但是,在我们考虑特异性之前,会先解析层,因此我们现在可以忽略选择器。

主层顺序预先建立,然后在内部添加子层。但是子层与其父层一起排序——这意味着所有 Montagues 将具有最低优先级,其次是所有 Capulets,然后 Verona 在层顺序中拥有最终决定权。因此,我们可以立即过滤掉只有 Verona 样式,它们优先。即使 * 选择器的特异性为零,它也会赢。

小心将通用选择器放在强大的层中!

在浏览器开发者工具中调试层冲突

Chrome、Safari、Firefox 和 Edge 浏览器都有开发者工具,允许您检查应用于页面上给定元素的样式。此元素检查器的样式面板将显示应用的选择器,按其级联优先级排序(最高优先级在顶部),然后是下面的继承样式。由于任何原因未应用的样式通常会变灰,甚至会被划掉——有时还会提供有关样式未应用原因的附加信息。这是调试级联任何方面(包括层冲突)时首先要查看的地方。

Safari Technology Preview 和 Firefox Nightly 已经在此面板中显示(并排序)级联层。预计此工具将在与级联层同时在稳定版本中推出。每个选择器的层都列在选择器本身的正上方。

Showing CSS Cascade Layers in Safari DevTools.
Safari
Showing CSS Cascade Layers in FireFox DevTools.
Firefox

Chrome/Edge 正在开发类似的工具,并希望在级联层进入稳定版本时在 Canary(夜间)版本中提供它们。随着这些工具的更改和改进,我们将在此处进行更新。

浏览器支持和回退

此浏览器支持数据来自 Caniuse,其中包含更多详细信息。数字表示浏览器支持该版本及更高版本的功能。

桌面

ChromeFirefoxIEEdgeSafari
9997不支持9915.4

移动设备/平板电脑

Android ChromeAndroid FirefoxAndroidiOS Safari
12712712715.4

由于层旨在作为整个 CSS 架构的基础构建块,因此很难想象像其他 CSS 功能那样构建手动回退。回退可能涉及复制大量的代码,使用不同的选择器来管理层叠层——或者提供一个更简单的回退样式表。

使用 @supports 查询功能支持

CSS 中有一个 @supports 功能,允许作者测试对 @layer 和其他 at 规则的支持。

@supports at-rule(@layer) {
  /* code applied for browsers with layer support */
}

@supports not at-rule(@layer) {
  /* fallback applied for browsers without layer support */
}

但是,目前还不清楚何时会在浏览器中支持此查询本身。

目前还没有关于从 html <link> 标签对整个样式表进行分层的语法的官方规范,但有一个 正在开发的提案。该提案包含一个新的 layer 属性,可用于将样式分配给命名或匿名层。

<!-- styles imported into to the <layer-name> layer -->
<link rel="stylesheet" href="example.css" layer="<layer-name>">

<!-- styles imported into to a new anonymous layer -->
<link rel="stylesheet" href="example.css" layer>

但是,不支持 layer 属性的旧浏览器将完全忽略它,并继续加载样式表而不进行任何分层。结果可能非常出乎意料。因此,该提案还扩展了现有的 media 属性,使其允许在 support() 函数中进行功能支持查询。

这将允许我们根据对分层的支持使分层链接成为条件。

<link rel="stylesheet" layer="bootstrap" media="supports(at-rule(@layer))" href="bootstrap.css">

潜在的 polyfill 和解决方法

主要浏览器都已转向 “常青”模型,更新会以相当短的发布周期推送给用户。即使是 Safari 也会在其较为罕见的主要版本之间的“补丁”更新中定期发布新功能。

这意味着我们可以预期这些功能的浏览器支持会非常快地增加。对于我们中的许多人来说,在短短几个月内就开始使用层可能是合理的,而无需过多担心旧浏览器。

对于其他人来说,可能需要更长的时间才能适应原生浏览器支持。还有很多其他方法可以管理层叠,使用选择器、自定义属性和其他工具。理论上也可以模拟(或 polyfill)基本行为。有人正在开发该 polyfill,但也不清楚何时才能准备好。

更多资源

CSS Cascade Layers 仍在发展中,但已经有许多资源,包括文档、文章、视频和演示,可以帮助您更加熟悉层及其工作原理。

参考

文章

视频

演示