CSS @layer

在真实开发中,我们经常会受困于 CSS 的优先级问题。

比如我们使用了一个第三方库,但是第三方库的样式不符合我们的要求,我们想进行修改,为了修改成功,要么我们需要以更高的权重覆盖库的样式,又或者通过 !important 强行将优先级调到最高,后续在别处又要覆盖此处的优先级时,只能使用权重更高的 !important,如此反复,我们很难知道最终到底是哪个样式在起作用。

另外,CSS 规则的最终级联结果还受到资源加载顺序的影响,不同的加载顺序会对最终的样式造成不同的影响,所以我们必须要严格按照某种顺序加载 CSS 样式才能达到我们预期的效果,这种顺序的维护是比较脆弱的,因为如果后续引入新的样式,我们都需要保证新样式插入到正确的位置以保证没有问题。

基于这个背景,CSS 规范引入了 @layer 规则,@layer 可以将样式组织到不同的​级联层中,并为这些层建立明确的优先级关系。因此,我们可以将第三方库放入较低优先级的级联层,再在更高优先级的级联层中编写覆盖样式,而不必不断提高选择器权重。

语法

在 CSS 中,我们通过 @layer 来声明一个级联层,根据是否有名称可以分为命名层和匿名层:

  • 命名层,可以包含语法块,也可以不包含

    CSS 复制代码
    @layer layer-name;
    @layer layer-name {
      .btn {
        color: red;  
      }  
    }

    也可以一次性声明多个级联层,级联层之间使用逗号分隔

    CSS 复制代码
    @layer reset, components, layout;
  • 匿名层,只有语法块,没有名称

    CSS 复制代码
    @layer {
      .btn {
        color: blue;  
      }  
    }

对于命名层,多次声明的相同名称的级联层是同一个 级联层

CSS 复制代码
@layer components {
  .button {
    padding: 0.5rem 1rem;
  }
}

/* 仍然是同一个 components 层 */
@layer components {
  .card {
    padding: 1rem;
  }
}

但是对于匿名层,每次都会创建一个新的级联层

CSS 复制代码
@layer {
  .button {
    padding: 0.5rem 1rem;
  }
}

@layer {
  .card {
    padding: 1rem;
  }
}

级联层还可以进行嵌套

CSS 复制代码
@layer framework {
  @layer reset {
    button {
      appearance: none;
    }
  }

  @layer components {
    .button {
      padding: 0.5rem 1rem;
    }
  }
}

也可以通过点号定义嵌套级联层

CSS 复制代码
@layer framework.components {
  .card {
    padding: 1rem;
  }
}

除了通过 @layer 来定义级联层,还可以在 @import 来导入样式的时候指定级联层

CSS 复制代码
@import url("./normalize.css") layer(reset);
@import url("./component-library.css") layer(vendor);

也可以导入到嵌套级联层

CSS 复制代码
@import url("./buttons.css") layer(framework.components);

级联层的优先级

我们先讨论不带 !important 的普通声明。级联层 的优先级和级联层​第一次声明的顺序有关,后声明的 级联层具有更高的优先级。

CSS 复制代码
@layer components {
  .button {
    color: blue;
  }
}

@layer reset {
  button {
    color: green;
  }
}

@layer utilities {
  .text-red {
    color: red;
  }
}

/* 优先级和第一次声明的顺序有关
   后续声明的同名 layer 不会修改 layer 的优先级 */
@layer components {
  .text-pink {
    color: pink;
  }
}

以上声明的优先级顺序为

CSS 复制代码
components < reset < utilities

需要注意的是,即使声明的级联层不带样式块,也是有效声明,也会创建一个级联层

CSS 复制代码
@layer components;
@layer utilities;

这个时候就已经确定了级联层的优先级关系

CSS 复制代码
components < utilities

通过 @import 定义的级联层也能定义优先级关系

CSS 复制代码
@import url("./vendor.css") layer(vendor);
@import url("./reset.css") layer(reset);

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

如果没有其他提前声明,那么顺序就是

CSS 复制代码
vendor < reset < components

与级联层内的样式相比,未分层样式具有更高的优先级。也就是说,写在 @layer 外的样式会优先于所有 @layer 内的样式,与它们出现的先后顺序无关。可以把未分层规则理解为处于当前层级的隐式最终层,位于所有命名层或匿名层之后。

CSS 复制代码
.button {
  color: black;
}

@layer components {
  #app .page .button {
    color: blue;
  }
}

@layer utilities {
  .button {
    color: red;
  }
}

以上代码定义的优先级是

CSS 复制代码
components < utilities < 未分层样式

对于嵌套级联层,会先比较父级联层的优先级。只有父级联层相同时,才会根据子级联层的声明顺序比较优先级

CSS 复制代码
@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;
    }
  }
}

以上代码定义的优先级是

CSS 复制代码
framework.reset
<
framework.components
<
framework 自身直接规则
<
application.base
<
application.utilities
<
application 自身直接规则
<
顶层未分层规则

我们还可以在查询条件中定义级联层,但如果级联层的顺序依赖这些条件规则,不同环境下就可能形成不同的优先级关系。先看下面的代码

CSS 复制代码
@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 的声明放在最上面。

CSS 复制代码
@layer theme, layout;

@media (min-width: 800px) {
  @layer layout {
    /* ... */
  }
}

@media (prefers-color-scheme: dark) {
  @layer theme {
    /* ... */
  }
}

不管查询条件是否满足,级联层的优先级永远是 theme < layout

浏览器优先级判定

前面的示例只比较了级联层。实际应用中,级联层只是浏览器决定最终样式的其中一个环节。

现在我们整体看下浏览器是如何最终决定一个元素的样式的,以及级联层优先级在什么时候考虑。

为了便于理解,下面只讨论常见的普通作者样式,不覆盖 transitionanimation@scope 等特殊情况。

如果有多个冲突的样式选中了同一个元素,浏览器会按照如下顺序来决定最终样式:

  1. 来源

首先样式的来源可以分为三类

CSS 复制代码
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

CSS 复制代码
<x-button></x-button>

有两个样式都选中了此按钮

CSS 复制代码
/* Shadow DOM 内部 */
:host {
  color: blue;
}

/* 外部页面 */
x-button {
  color: red;
}

它们来自于不同的封装上下文,规范规定外部上下文优先,因此外部页面可以方便地覆盖组件为 host 设置的默认值 3. 内联样式

是否是通过 style="" 直接定义的内联样式,内联样式具有更高的优先级 4. 级联层

然后才是我们上述讨论的级联层优先级比较。对于普通声明,后声明的级联层获胜 5. 选择器权重

如果位于同一级联层,那么就得看选择器的权重,权重高的获胜 6. 源码顺序

如果权重也相同,那么就看在源码中出现的顺序,出现在后面的样式优先级高于出现在前面的

!important 的优先级反转

以上优先级的讨论都是基于非 !important 的讨论。我们知道 !important 可以将优先级强行提到最高,它的优先级比较发生在正常样式比较之前。

但是如果有多个 !important 的样式发生了冲突,优先级是怎样呢,和上面的优先级是一样的吗?答案是部分优先级会发生反转。

首先,浏览器比较的顺序没有发生变化,依旧是如下流程

  1. 来源

多个冲突的 !important 样式,优先级是 Author Origin < User Origin < User-Agent Origin。

也就是说浏览器定义的 !important 样式优先级最高,没有办法可以将其覆盖,一个真实的例子,Firefox 对于全屏元素包含类似规则

CSS 复制代码
: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;
}

如果页面尝试通过修改全屏元素的样式,是不生效的

CSS 复制代码
video:fullscreen {
  position: absolute !important;
  width: 300px !important;
  margin: 100px !important;
}

实际上,根据规范,CSS Transition 样式的优先级是最高的,从技术上讲并非不可覆盖 UA !important 样式,但不建议作为覆盖 UA 行为的方法。

CSS 复制代码
CSS Transition >
UA !important >
User !important >
Author !important >
CSS Animation >
Author normal >
User normal >
UA normal >
  1. 封装上下文

内层的样式大于外层的样式

CSS 复制代码
/* Shadow DOM 内部 */
:host {
  color: blue !important;
}

/* 外部页面 */
x-button {
  color: red !important;
}
  1. 内联样式

这里没有变化,因为在这一层没有内部比较的优先级 4. 级联层

级联层的 !important 样式优先级发生了反转,先声明的级联层的 !important 样式优先级高于后声明的级联层样式

CSS 复制代码
@layer reset, components, utilities;

@layer reset {
  .button {
    color: red !important;
  }
}

@layer utilities {
  #app .page .button {
    color: blue !important;
  }
}

对于 !important 的样式优先级是

CSS 复制代码
reset > components > utilities > 未分层
  1. 选择器权重

这里优先级顺序没有反转,权重高的 !important 样式优先级依旧高于权重低的

CSS 复制代码
@layer components {
  #app .button {
    color: blue !important;
  }

  .button {
    color: red !important;
  }
}

最终生效的是 color: blue !important; 6. 源码顺序

这部分的优先级也没有反转,相同权重,出现后面的样式优先级依旧高于前面的

CSS 复制代码
@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,和 initialinheritunsetrevert 一样,可以作为几乎任何 CSS 属性的完整值,它的作用是忽略当前级联层对这个属性的贡献,重新从更低优先级的层开始计算,让高层级的级联层放弃控制权。

revert 会撤销当前 Origin 对该属性的影响,并从优先级更低的 Origin 重新进行级联。例如,作者样式中的 revert 会继续考虑用户样式和浏览器默认样式。revert 是按 Cascade Origin 回滚,而 revert-layer 可以理解为按 Cascade Layer 回滚。

假设有四层级联层

CSS 复制代码
@layer reset, base, components, utilities;

正常的优先级是

CSS 复制代码
reset < base < components < utilities

有如下样式

CSS 复制代码
@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;
  }
}
HTML 复制代码
<button class="button text-red restore-component-color">
  Save
</button>

根据我们上面的级联层规则,utilities 内部的

CSS 复制代码
.restore-component-color {
  color: revert-layer;
}

优先级最高,赢得了 color 的竞争,它取值为 revert-layer,就是告诉浏览器,重新计算 color 的样式,忽略当前级联层为该元素声明的 color,也就是说,utilities 级联层的 .text-redcolor 的声明会被一并忽略。

revert-layer 会忽略当前声明所在级联层对该属性的影响,并继续按照当前 Origin 和重要性级别,寻找更低优先级的级联结果。也就是

CSS 复制代码
@layer components {
  .button {
    color: white;
  }
}

需要注意的是,这里不是回退到上一层,而是回退到上一个优先级比较低的级联样式

CSS 复制代码
@layer reset, base, components, utilities;

@layer reset {
  .button {
    color: black;
  }
}

@layer base {
  /* 没有设置 color */
}

@layer components {
  /* 没有匹配 .button 的 color */
}

@layer utilities {
  .button {
    color: revert-layer;
  }
}

对于如上情况,componentsbase 都没有设置 color 样式,那么就会回退到

CSS 复制代码
@layer reset {
  .button {
    color: black;
  }
}

所以,如果当前 Origin 和重要性级别中不存在更低优先级的级联层声明,revert-layer 可能继续回退到其他 Origin;在这种情况下,它的结果可能与 revert 相同,但两者的语义仍然不同。

revert-layer 并不是简单地回退到当前父层的下一个子层,而是继续沿整个级联层树寻找更低优先级的可用结果。

CSS 复制代码
@layer framework, application;

@layer framework {
  .target {
    color: blue;
  }
}

@layer application {
  @layer base, components;

  @layer base {
    .target {
      color: revert-layer;
    }
  }
}

application.baseapplication 内部最低的子层,但它并不是整个 Author Origin 的最低层。退出 application.base 后,浏览器仍可能找到更低优先级的顶层父层,因此,application.base 中的 revert-layer 会回退到 framework 中的声明

CSS 复制代码
@layer framework {
  .target {
    color: blue;
  }
}

工程实践

在顶部入口集中声明顺序

推荐在入口文件中集中声明顺序

CSS 复制代码
@layer reset, vendor, tokens, base, components, utilities, overrides;

不要让每个文件自己首次创建级联层,否则级联层的顺序就会依赖于文件的加载顺序

CSS 复制代码
/* a.css */
@layer components {
}

/* b.css */
@layer reset {
}

三方库 !important 样式的覆盖

为了能够覆盖三方库的样式,我们一般会将应用层的样式声明在后面

CSS 复制代码
@layer vendor, application;

@import url("./vendor.css") layer(vendor);

但对于 !important 规则中 vendor > application,无法在 application 这个级联层覆盖 vendor!important 样式,对于这种情况,我们可以在最前方加上一个 important-overrides 级联层

CSS 复制代码
@layer important-overrides, vendor, application;

@layer important-overrides {
  .btn {
    color: red !important;
  }
}

并规定只有处理第三方 !important 或关键约束时才能使用它。

命名空间

库作者应避免使用 componentsbase 等通用的顶层级联层名称

CSS 复制代码
/* 不理想 */
@layer components;

更建议使用嵌套的命名空间

CSS 复制代码
@layer acme {
  @layer reset, tokens, components;
}

使用库名或其他足够唯一的名称作为顶层命名空间,可以避免和应用层的命名发生冲突,导致同名级联层意外合并。

相关推荐
李宸净7 小时前
Web自动化测试selenium+python
前端·python·selenium
淡海水8 小时前
06-04-YooAsset源码-Unity加密解密服务
前端·unity·性能优化·c#·游戏引擎·yooasset
trigger3339 小时前
desk-health-web-community-post
前端
倒流时光三十年9 小时前
Logback 系列(7):常用 Appender(控制台 / 文件 / 滚动文件)
java·前端·logback
Csvn9 小时前
页面「穿越」之谜:React 中因 key 值不正确导致的渲染 Bug 排查实战
前端
浅水壁虎9 小时前
前端技术_TypeScript(第一章)
前端
laboratory agent开发10 小时前
企业AI Agent落地前,先回答四个工程问题
java·前端·人工智能
微三云 - 廖会灵 (私域系统开发)10 小时前
电商系统国际化架构设计:多语言、多币种、多时区、多税制的全链路实现
java·前端·数据库
不简说11 小时前
JS 代码技巧 vol.5 — 10 个错误处理套路,从 try/catch 到 Error Boundary
前端·javascript·程序员