在真实开发中,我们经常会受困于 CSS 的优先级问题。
比如我们使用了一个第三方库,但是第三方库的样式不符合我们的要求,我们想进行修改,为了修改成功,要么我们需要以更高的权重覆盖库的样式,又或者通过 !important 强行将优先级调到最高,后续在别处又要覆盖此处的优先级时,只能使用权重更高的 !important,如此反复,我们很难知道最终到底是哪个样式在起作用。
另外,CSS 规则的最终级联结果还受到资源加载顺序的影响,不同的加载顺序会对最终的样式造成不同的影响,所以我们必须要严格按照某种顺序加载 CSS 样式才能达到我们预期的效果,这种顺序的维护是比较脆弱的,因为如果后续引入新的样式,我们都需要保证新样式插入到正确的位置以保证没有问题。
基于这个背景,CSS 规范引入了 @layer 规则,@layer 可以将样式组织到不同的级联层中,并为这些层建立明确的优先级关系。因此,我们可以将第三方库放入较低优先级的级联层,再在更高优先级的级联层中编写覆盖样式,而不必不断提高选择器权重。
语法
在 CSS 中,我们通过 @layer 来声明一个级联层,根据是否有名称可以分为命名层和匿名层:
-
命名层,可以包含语法块,也可以不包含
@layer layer-name; @layer layer-name { .btn { color: red; } }也可以一次性声明多个级联层,级联层之间使用逗号分隔
@layer reset, components, layout; -
匿名层,只有语法块,没有名称
@layer { .btn { color: blue; } }
对于命名层,多次声明的相同名称的级联层是同一个 级联层
@layer components {
.button {
padding: 0.5rem 1rem;
}
}
/* 仍然是同一个 components 层 */
@layer components {
.card {
padding: 1rem;
}
}
但是对于匿名层,每次都会创建一个新的级联层
@layer {
.button {
padding: 0.5rem 1rem;
}
}
@layer {
.card {
padding: 1rem;
}
}
级联层还可以进行嵌套
@layer framework {
@layer reset {
button {
appearance: none;
}
}
@layer components {
.button {
padding: 0.5rem 1rem;
}
}
}
也可以通过点号定义嵌套级联层
@layer framework.components {
.card {
padding: 1rem;
}
}
除了通过 @layer 来定义级联层,还可以在 @import 来导入样式的时候指定级联层
@import url("./normalize.css") layer(reset);
@import url("./component-library.css") layer(vendor);
也可以导入到嵌套级联层
@import url("./buttons.css") layer(framework.components);
级联层的优先级
我们先讨论不带 !important 的普通声明。级联层 的优先级和级联层第一次声明的顺序有关,后声明的 级联层具有更高的优先级。
@layer components {
.button {
color: blue;
}
}
@layer reset {
button {
color: green;
}
}
@layer utilities {
.text-red {
color: red;
}
}
/* 优先级和第一次声明的顺序有关
后续声明的同名 layer 不会修改 layer 的优先级 */
@layer components {
.text-pink {
color: pink;
}
}
以上声明的优先级顺序为
components < reset < utilities
需要注意的是,即使声明的级联层不带样式块,也是有效声明,也会创建一个级联层
@layer components;
@layer utilities;
这个时候就已经确定了级联层的优先级关系
components < utilities
通过 @import 定义的级联层也能定义优先级关系
@import url("./vendor.css") layer(vendor);
@import url("./reset.css") layer(reset);
@layer components {
/* ... */
}
如果没有其他提前声明,那么顺序就是
vendor < reset < components
与级联层内的样式相比,未分层样式具有更高的优先级。也就是说,写在 @layer 外的样式会优先于所有 @layer 内的样式,与它们出现的先后顺序无关。可以把未分层规则理解为处于当前层级的隐式最终层,位于所有命名层或匿名层之后。
.button {
color: black;
}
@layer components {
#app .page .button {
color: blue;
}
}
@layer utilities {
.button {
color: red;
}
}
以上代码定义的优先级是
components < utilities < 未分层样式
对于嵌套级联层,会先比较父级联层的优先级。只有父级联层相同时,才会根据子级联层的声明顺序比较优先级
@layer framework, application;
@layer framework {
@layer reset, components;
@layer reset {
.button {
color: gray;
}
}
@layer components {
.button {
color: blue;
}
}
}
@layer application {
@layer base, utilities;
@layer base {
.button {
color: green;
}
}
@layer utilities {
.button {
color: red;
}
}
}
以上代码定义的优先级是
framework.reset
<
framework.components
<
framework 自身直接规则
<
application.base
<
application.utilities
<
application 自身直接规则
<
顶层未分层规则
我们还可以在查询条件中定义级联层,但如果级联层的顺序依赖这些条件规则,不同环境下就可能形成不同的优先级关系。先看下面的代码
@media (min-width: 800px) {
@layer layout {
/* ... */
}
}
@media (prefers-color-scheme: dark) {
@layer theme {
/* ... */
}
}
@layer theme, layout;
这段代码在不同环境下会形成不同的级联层顺序:
- 当宽屏条件成立时,源码中最先参与层顺序建立的是
layout,因此顺序为layout < theme; - 当宽屏条件不成立、深色模式条件成立时,
theme最先参与层顺序建立,因此顺序为theme < layout; - 当两个条件都不成立时,最后的
@layer theme, layout决定顺序,因此仍是theme < layout。
也就是说,查询条件是否成立可能影响级联层第一次参与级联的位置,进而改变层的优先级关系。为了让级联层的优先级不受运行环境影响,我们应该提前确定好级联层的创建顺序,也就是把 @layer theme, layout 的声明放在最上面。
@layer theme, layout;
@media (min-width: 800px) {
@layer layout {
/* ... */
}
}
@media (prefers-color-scheme: dark) {
@layer theme {
/* ... */
}
}
不管查询条件是否满足,级联层的优先级永远是 theme < layout。
浏览器优先级判定
前面的示例只比较了级联层。实际应用中,级联层只是浏览器决定最终样式的其中一个环节。
现在我们整体看下浏览器是如何最终决定一个元素的样式的,以及级联层优先级在什么时候考虑。
为了便于理解,下面只讨论常见的普通作者样式,不覆盖
transition、animation、@scope等特殊情况。
如果有多个冲突的样式选中了同一个元素,浏览器会按照如下顺序来决定最终样式:
- 来源
首先样式的来源可以分为三类
Author Origin 网站作者样式
User Origin 用户为了满足阅读需求而要求浏览器额外应用的 CSS,实际开发中较少直接接触
User-Agent Origin 浏览器默认样式
对于普通声明,Origin 的优先级是 Author Origin > User Origin > User-Agent Origin。
浏览器先看样式的 Origin 来自哪里,在发生冲突时,根据 Author Origin > User Origin > User-Agent Origin 决定哪个胜出 2. 封装上下文
此条规则适用于 Shadow DOM 的场景,比如对于一个 Web Component
<x-button></x-button>
有两个样式都选中了此按钮
/* Shadow DOM 内部 */
:host {
color: blue;
}
/* 外部页面 */
x-button {
color: red;
}
它们来自于不同的封装上下文,规范规定外部上下文优先,因此外部页面可以方便地覆盖组件为 host 设置的默认值 3. 内联样式
是否是通过 style="" 直接定义的内联样式,内联样式具有更高的优先级
4. 级联层
然后才是我们上述讨论的级联层优先级比较。对于普通声明,后声明的级联层获胜 5. 选择器权重
如果位于同一级联层,那么就得看选择器的权重,权重高的获胜 6. 源码顺序
如果权重也相同,那么就看在源码中出现的顺序,出现在后面的样式优先级高于出现在前面的
!important 的优先级反转
以上优先级的讨论都是基于非 !important 的讨论。我们知道 !important 可以将优先级强行提到最高,它的优先级比较发生在正常样式比较之前。
但是如果有多个 !important 的样式发生了冲突,优先级是怎样呢,和上面的优先级是一样的吗?答案是部分优先级会发生反转。
首先,浏览器比较的顺序没有发生变化,依旧是如下流程
- 来源
多个冲突的 !important 样式,优先级是 Author Origin < User Origin < User-Agent Origin。
也就是说浏览器定义的 !important 样式优先级最高,没有办法可以将其覆盖,一个真实的例子,Firefox 对于全屏元素包含类似规则
:fullscreen:not(:root) {
position: fixed !important;
inset: 0 !important;
width: 100% !important;
height: 100% !important;
margin: 0 !important;
min-width: 0 !important;
max-width: none !important;
min-height: 0 !important;
max-height: none !important;
box-sizing: border-box !important;
transform: none !important;
}
如果页面尝试通过修改全屏元素的样式,是不生效的
video:fullscreen {
position: absolute !important;
width: 300px !important;
margin: 100px !important;
}
实际上,根据规范,CSS Transition 样式的优先级是最高的,从技术上讲并非不可覆盖 UA
!important样式,但不建议作为覆盖 UA 行为的方法。CSS Transition > UA !important > User !important > Author !important > CSS Animation > Author normal > User normal > UA normal >
- 封装上下文
内层的样式大于外层的样式
/* Shadow DOM 内部 */
:host {
color: blue !important;
}
/* 外部页面 */
x-button {
color: red !important;
}
- 内联样式
这里没有变化,因为在这一层没有内部比较的优先级 4. 级联层
级联层的 !important 样式优先级发生了反转,先声明的级联层的 !important 样式优先级高于后声明的级联层样式
@layer reset, components, utilities;
@layer reset {
.button {
color: red !important;
}
}
@layer utilities {
#app .page .button {
color: blue !important;
}
}
对于 !important 的样式优先级是
reset > components > utilities > 未分层
- 选择器权重
这里优先级顺序没有反转,权重高的 !important 样式优先级依旧高于权重低的
@layer components {
#app .button {
color: blue !important;
}
.button {
color: red !important;
}
}
最终生效的是 color: blue !important;
6. 源码顺序
这部分的优先级也没有反转,相同权重,出现后面的样式优先级依旧高于前面的
@layer components {
.button {
color: blue !important;
}
.button {
color: red !important;
}
}
最终生效的是 color: red !important;
revert-layer
CSS 引入了一个新的:content-link[全局关键字]{refId=381 contentId=323 contentType=card},revert-layer,和 initial、inherit、unset、revert 一样,可以作为几乎任何 CSS 属性的完整值,它的作用是忽略当前级联层对这个属性的贡献,重新从更低优先级的层开始计算,让高层级的级联层放弃控制权。
revert 会撤销当前 Origin 对该属性的影响,并从优先级更低的 Origin 重新进行级联。例如,作者样式中的 revert 会继续考虑用户样式和浏览器默认样式。revert 是按 Cascade Origin 回滚,而 revert-layer 可以理解为按 Cascade Layer 回滚。
假设有四层级联层
@layer reset, base, components, utilities;
正常的优先级是
reset < base < components < utilities
有如下样式
@layer reset {
button {
color: black;
}
}
@layer base {
button {
font-family: system-ui;
}
}
@layer components {
.button {
color: white;
background: royalblue;
}
}
@layer utilities {
.text-red {
color: red;
}
.restore-component-color {
color: revert-layer;
}
}
<button class="button text-red restore-component-color">
Save
</button>
根据我们上面的级联层规则,utilities 内部的
.restore-component-color {
color: revert-layer;
}
优先级最高,赢得了 color 的竞争,它取值为 revert-layer,就是告诉浏览器,重新计算 color 的样式,忽略当前级联层为该元素声明的 color,也就是说,utilities 级联层的 .text-red 对 color 的声明会被一并忽略。
revert-layer 会忽略当前声明所在级联层对该属性的影响,并继续按照当前 Origin 和重要性级别,寻找更低优先级的级联结果。也就是
@layer components {
.button {
color: white;
}
}
需要注意的是,这里不是回退到上一层,而是回退到上一个优先级比较低的级联样式
@layer reset, base, components, utilities;
@layer reset {
.button {
color: black;
}
}
@layer base {
/* 没有设置 color */
}
@layer components {
/* 没有匹配 .button 的 color */
}
@layer utilities {
.button {
color: revert-layer;
}
}
对于如上情况,components 和 base 都没有设置 color 样式,那么就会回退到
@layer reset {
.button {
color: black;
}
}
所以,如果当前 Origin 和重要性级别中不存在更低优先级的级联层声明,revert-layer 可能继续回退到其他 Origin;在这种情况下,它的结果可能与 revert 相同,但两者的语义仍然不同。
revert-layer 并不是简单地回退到当前父层的下一个子层,而是继续沿整个级联层树寻找更低优先级的可用结果。
@layer framework, application;
@layer framework {
.target {
color: blue;
}
}
@layer application {
@layer base, components;
@layer base {
.target {
color: revert-layer;
}
}
}
application.base 是 application 内部最低的子层,但它并不是整个 Author Origin 的最低层。退出 application.base 后,浏览器仍可能找到更低优先级的顶层父层,因此,application.base 中的 revert-layer 会回退到 framework 中的声明
@layer framework {
.target {
color: blue;
}
}
工程实践
在顶部入口集中声明顺序
推荐在入口文件中集中声明顺序
@layer reset, vendor, tokens, base, components, utilities, overrides;
不要让每个文件自己首次创建级联层,否则级联层的顺序就会依赖于文件的加载顺序
/* a.css */
@layer components {
}
/* b.css */
@layer reset {
}
三方库 !important 样式的覆盖
为了能够覆盖三方库的样式,我们一般会将应用层的样式声明在后面
@layer vendor, application;
@import url("./vendor.css") layer(vendor);
但对于 !important 规则中 vendor > application,无法在 application 这个级联层覆盖 vendor 的 !important 样式,对于这种情况,我们可以在最前方加上一个 important-overrides 级联层
@layer important-overrides, vendor, application;
@layer important-overrides {
.btn {
color: red !important;
}
}
并规定只有处理第三方 !important 或关键约束时才能使用它。
命名空间
库作者应避免使用 components、base 等通用的顶层级联层名称
/* 不理想 */
@layer components;
更建议使用嵌套的命名空间
@layer acme {
@layer reset, tokens, components;
}
使用库名或其他足够唯一的名称作为顶层命名空间,可以避免和应用层的命名发生冲突,导致同名级联层意外合并。