嵌套解决「写得长」,@layer 解决「谁赢」

① 原生嵌套:一条规则里把上下文写全

卡片标题(h3 的 margin-top 被父级嵌套清掉了)

.meta 的字号与颜色来自 & .meta

chip hot

带 .is-active 的卡片

来自 &.is-active —— 注意 & 与修饰符之间没有空格,有空格就变成了后代选择器

/* 展开规则:& 代表「外层的选择器原文」 */
.card { &:hover { … } }        → .card:hover { … }
.card { & .meta { … } }        → .card .meta { … }
.card { & + .card { … } }      → .card + .card { … }
.btn { &--primary { … } }      → .btn--primary { … }        /* BEM 修饰符 */
.card { :is(&, .aside) h3 {} } → :is(.card, .aside) h3 {}   /* & 可以出现在参数里 */

/* 最前面是 & 时整个 & 可省略,所以可以写成 .card { h3 { … } } —— 不建议:
   省略后隐含的是「后代」关系,选择器变成 .card h3(权重与匹配范围都变了),可读性也下降 */

/* 条件规则可以写在规则内部:媒体查询、@supports、@container 都支持 */
.menu { @media (max-width: 24rem) { & { font-size: 13px; } } }

浏览器基线:CSS 原生嵌套(含 &、条件规则内嵌)在 Safari 16.5 / Chrome 112 / Firefox 117 一线已全量支持,属 Baseline 可用;需要兼容旧内核时请让构建工具(Sass、PostCSS、Lightning CSS)降级展开,不要在生产里裸写。

② 嵌套的代价:选择器膨胀

/* 四层嵌套写起来爽,展开后是这种怪物: */
.page .section .list .item:hover .title span { … }     /* 权重 (0,4,3),几乎没法覆盖 */
/* 症状:想改一个标题颜色,只能加 !important;改一个 DOM 结构,样式全断 */
/* 经验值:
   ① 嵌套深度 ≤ 2 层(组件本体 + 一个子元素/状态);
   ② 不要把布局上下文写进组件(组件不该知道自己被放在哪个 .page .section 里);
   ③ 需要跨层级联动时用「变量或状态类」而不是选择器链(见第十四章组件变量);
   ④ 权重问题回第四章解决:降低 specificity 靠更合理的类名,不是靠嵌套堆叠。 */

③ @layer:把「谁优先」写成一句声明

/* 第一行就声明层顺序:越靠后越优先 */
@layer reset, base, vendors, components, overrides;

@layer base     { .panel { border-color: #bf8700; } }
@layer vendors  { .panel { border-color: #cf222e; } }   /* 后声明的层赢,哪怕写在前面 */
这是一个 .panel:红色边框来自 vendors 层,白色背景来自 components 层,字号 13px 来自未分层那一条。

上面这个 .panel 现在是红色边框:它来自 vendors 层,而 vendors 在声明顺序里排在 base 之后 —— 与代码书写顺序无关,只与层顺序有关。这就是 @layer 解决的老问题:第三方库/工具类的样式总在悄悄抢权重。

④ 完整的层叠顺序(必须记住这一张表)

从高到低说明
过渡 / 动画声明动画进行中的值优先于一切普通声明(第十八章)
用户 !important浏览器扩展、用户样式表
作者 !important:未分层最高,各层之间则反向(越早声明的层越优先)这条最反直觉:!important 会反转层的顺序
未分层(unlayered)作者样式比所有已声明层都高——所以「随手加的补丁」默认就是最强,长期看反而是坑
分层作者样式按 @layer a, b, c; 的顺序,c > b > a
用户样式表 / 浏览器默认依次降低
/* 未分层样式「压过」所有层,想让补丁被层管理,就把它放进最末的层: */
@layer overrides { .panel { border-color: #2da44e; } }
/* 匿名层与同名层:同名 @layer 会合并;嵌套写法 components.button.primary 形成子层 */
/* 让第三方 CSS 进层而不用改它的源码:@layer vendors { @import "reset.css"; } */

浏览器基线:@layer 与 layer() 函数在 Chrome 99 / Safari 15.4 / Firefox 103 一线已全量支持。用法建议:项目从零开始才引入分层;给已有大项目补层容易和未分层样式打架。

⑤ 实用层集合起点

@layer reset, base, layout, components, utilities, overrides;

@layer reset     { *, *::before, *::after { box-sizing: border-box; margin: 0; } }
@layer base      { body { font: 400 1rem/1.7 system-ui, sans-serif; } }
@layer layout    { .doc-layout { display: grid; grid-template-columns: 216px 1fr 232px; } }
@layer components{ .card { … } }
@layer utilities { .sr-only { … } .mt-0 { margin-top: 0 !important; } }  /* 工具类放最后的层,才不需要 !important */

这套顺序也是本站 main.css 的组织思路(单文件、用注释分节)。如果哪天真要分层,把注释分节改成 @layer 即可,选择器无需重写。

⑥ 自查

  1. 「嵌套写了没生效」→ 检查是否漏了 &(&&--hot 与 & --hot 是两个意思)。
  2. 「层里的样式被随手一行盖掉」→ 那就是未分层样式默认最高,符合规范;把它挪进 overrides 层。
  3. 「加了 !important 反而输给早层」→ 正确行为,见④表格。
  4. 「DevTools 里看不懂谁赢」→ Styles 面板现在会显示层名与权重,逐条取消勾选验证(第二十章)。