本文为通用技术教程,示例均可在现代浏览器中直接复现。
一个诡异的 Bug
假设站点里有这样一条再普通不过的基础样式:
a {
color: inherit;
text-decoration: none;
}
然后给某个链接加一个 Tailwind 工具类换色:
<a href="/subscribe" class="text-[var(--ink-100)]">订阅</a>
预期是文字变成亮色,实际却纹丝不动——工具类“失效”了。检查特异性?类选择器(0,1,0)明明高于元素选择器(0,0,1)。检查源码顺序?把工具类挪到最后也没用。
问题不在特异性,也不在顺序,而在级联层。
级联层规则:未分层即最高优先
CSS Cascade Layers 的裁定顺序是:先比较“有没有层”,再比较特异性。
- 所有
@layer内的样式,彼此之间按层顺序裁定; - 未分层的作者样式,无条件赢过任何层里的样式——无论特异性多低。
Tailwind v4 把工具类输出在 @layer utilities 中。于是:
@layer utilities {
.text-brand {
color: var(--brand);
} /* 层内 */
}
a {
color: inherit;
} /* 层外 */
层外的 a 直接胜出。这解释了开头的 Bug:不是工具类没生成,而是它在层里“天生低人一等”。
顺带一提,这个规则还会让 hover: 变体悄悄失灵——a:hover { color: … } 若写在层外,即使特异性只有 (0,1,1) 也能压过层内的任何工具类。
修复:把元素级默认样式放进 base 层
Tailwind v4 在样式表开头声明了层顺序:
@layer theme, base, components, utilities;
只要把自己的元素级默认样式放进同名 base 层, utilities 就会按声明顺序正确地覆盖它:
@layer base {
html {
background: rgb(var(--bg));
}
body {
color: rgb(var(--text));
font-family: var(--font-sans);
}
a {
color: inherit;
text-decoration: none;
}
:focus-visible {
outline: 2px solid rgb(var(--accent));
}
}
修复后的裁定顺序变成:
- 浏览器默认(user agent);
base层的元素默认;utilities层的工具类——赢家。
反过来:哪些规则应该留在层外
层外规则是“终极王牌”,要克制地使用。适合留在层外的只有两类:
- Token 定义:
:root { --accent-primary: … }和主题覆盖html[data-accent='…']。它们是整套样式的地基,必须稳赢任何组件层的意外干扰。 - 刻意的兜底/重置:确信要压过一切工具类的历史包袱修复,配上注释说明原因。
一个判断标准:这条规则如果被某个工具类覆盖,是“坏了”还是“对了”?答案是“对了”,就该进层。
排查工具
遇到“类写了却不生效”,按这个顺序查:
- DevTools 的 Styles 面板:看规则是否出现。出现了但被划掉,看划掉它的规则来自哪一层(面板会标注
layer)。 - 控制台验证匹配:
document.querySelector(sel).matches('.text-\\[var\\(--x\\)\\]')。 - 检查层归属:
getComputedStyle不显示层级,最直观的方式是在 Styles 面板看每条规则旁边的 layer 徽标。
小结
级联层把“谁赢”从特异性博弈简化成了声明顺序:theme → base → components → utilities。基础样式进 base 层,工具类在 utilities 层稳赢;token 留在层外当地基。理解这一条,整个设计系统的层叠就再也不靠运气。