CSS @layer 实战笔记:层序才是优先级
先看一个让人血压升高的场景
项目接了个第三方 UI 库,需求是把默认按钮改成 12px 字重 700。打开 DevTools 一看,框架自带 .btn-primary .btn-icon 这种 0-3-1 的选择器,你想覆盖它,得写出:
body .app .content .btn-primary .btn-icon {
font-size: 12px;
font-weight: 700;
}
这还没完,下个迭代又来了个全局 reset 把 button { font-sizer: inherit } 横插一脚。于是你的样式表里到处都是 !important 和冗长选择器,每次改样式都要开一场「谁更长谁赢」的军备竞赛。
@layer 的思路:别比兵器了,直接排座次
级联层让优先级跟「层序」绑定,跟选择器特异性脱钩。你把第三方库塞进 layer(vendor),业务样式放普通未分层,就能用 .btn 平推掉框架里 0-3-1 的规则。
@layer reset, vendor, base, components, utilities;
@layer vendor {
.btn-primary .btn-icon { font-size: 14px; }
}
@layer components {
.btn { font-size: 12px; }
}
不写任何 !important,.btn 赢了。因为 components 写在 vendor 后面。
层序规则就这几条
- 前面的层低,后面的层高。在样式表最上面用
@layer声明一次层序,后面各文件里@layer components { ... }追加内容,座次早已锁定。 - 层之间完全无视特异性。
.btn就是能赢#main .btn-primary .btn-icon,只要前者所在的层靠后。 - 层内该比特异性还是比,逃不掉。
- 未分层样式(普通写在全局的规则)在普通声明中赢过所有分层样式,也是无视特异性。想在关键时刻强制覆盖,直接把规则写成未分层即可,全程不用
!important。
/* 未分层 */
.btn { color: red; }
@layer vendor {
body .btn { color: blue; } /* 层内特异性再高也输 */
}
/* 最终 red,因为是未分层。 */
!important层序反转。在!important声明里,先声明的层最猛。分层!important还压过未分层的!important。
@layer low, high;
@layer low {
.foo { color: red !important; }
}
@layer high {
.foo { color: blue !important; }
}
/* 结果是 red,因为 low 层声明在前,!important 下低层反而强。 */
这玩意平时真的用不上,但知道它存在,排查问题时少掉一半头发。
嵌套层和导入分层
嵌套层适合组件内部自己分小层:
@layer card {
@layer header, body, footer; /* 声明子层顺序 */
@layer body { ... }
}
/* 或者用点号 */
@layer card.body { ... }
第三方库直接用 @import 分层塞进低层:
@import url("lib.css") layer(vendor);
注意 @import 必须放在样式表最前面,只能排在 @charset 之后、纯声明层序的 @layer 之后。前面一旦出现普通规则,后面的 @import 直接忽略,而且浏览器无警告。
revert-layer:撤销本层的设定
某属性想回到上一层叠层的值,用 revert-layer:
@layer base {
.btn { border: 1px solid gray; }
}
@layer components {
.btn { border: 1px solid gray; border-radius: 999px; }
.btn.ghost { border-radius: revert-layer; } /* 回到 base 的值 */
}
别指望 @layer 解决两件事
作用域问题它管不了。 card.css 里写 @layer card { } 不表示选择器只在 .card 内生效,层级跟作用域没关系。
层序在第一次出现时即锁定。 你这文件里写 @layer a, b; 另一个文件开头写 @layer b, a; 没用,真正生效的是最先解析到的那个声明顺序。所以层序声明集中放入口文件最顶端,别零散分布。
分层 CSS 不会替换非分层 CSS。 你有重置需求,reset.css 可以当未分层放最底下做基线,再把分层叠加在它上面。
推荐落地层序
@layer reset, tokens, base, layout, components, utilities;
这个顺序的爽点:
utilities天然压过componentscomponents压过base- 第三方 reset 永远在最底,不用担心它哪天反扑
- 临时加样式写成未分层,直接最高优先
从 Baseline 2022(2022 年 3 月)开始,这东西在主流浏览器里都能用。我的建议:新项目直接用 @layer 搭骨架,老项目先在「新增模块」或「覆盖第三方样式」的场合单独用,不必等全站改造完。