我不知道的 CSS(01)— 选择器匹配、优先级与层叠
一、z-index 设了 9999 为什么还是不生效?
一、z-index 设了 9999 为什么还是不生效?
很多人遇到过这个场景:给元素设了 z-index: 9999,但它就是被另一个元素挡住了。凭直觉,9999 应该够大了吧?
问题往往不出在 z-index 的数字上,而出在层叠上下文和选择器优先级上。CSS 的样式最终由谁”胜出”,取决于一套完整的层叠机制——选择器匹配方向、特定性计算、层叠顺序,以及 2022 年新增的 @layer 规则。
这套机制,很多人天天在用,但真正说得清楚的并不多。
二、选择器为什么从右往左匹配?
写下 .container .item span 这个选择器时,直觉上浏览器应该先找 .container,再找 .item,最后找 span——从左到右。
但实际上,浏览器的匹配方向恰好相反:从右到左。
浏览器先找到页面上所有的 span 元素,然后逐个往上检查,看它的祖先中是否存在 .item,.item 的祖先中是否存在 .container。只要某一级验证失败,立刻放弃当前 span,检查下一个。
为什么这样做更快?考虑一个有 1000 个 DOM 节点的页面,其中只有 3 个 span。从右到左,只需要对 3 个 span 做祖先链验证。而从左到右,需要先找到所有 .container,再遍历它们的后代,搜索空间大得多。
下面这个例子可以直观感受差异:
<div class="container">
<div class="item">
<span>目标</span>
</div>
<div class="other">
<p>非目标</p>
<div>非目标</div>
<!-- ...假设这里有 500 个子元素 -->
</div>
</div>
对选择器 .container .item span,从右到左只需检查 1 个 span 的祖先链(2 次向上查找)。而从左到右,找到 .container 后需要遍历它所有后代,包括 .other 下的 500 个无关节点。
如果你只记住一句话:浏览器匹配选择器是从最右边的”键选择器”开始的,然后逐级向上验证。 这就是为什么把最具体的选择器放在最右边,能帮助浏览器更快完成匹配。
三、特定性的四级权重:不是简单的数字比大小
选择器匹配之后,下一个问题是:多条规则都命中了同一个元素,谁的样式生效?
答案是特定性(Specificity)。很多人知道”ID 选择器比 class 选择器优先级高”,但具体的计算规则经常被搞混。
特定性的计算基于四个层级,记为 (a, b, c, d):
(1) 行内样式(style="..."):权重 (1, 0, 0, 0)
(2) ID 选择器(#id):权重 (0, 1, 0, 0)
(3) 类选择器(.class)、属性选择器([type="text"])、伪类(:hover):权重 (0, 0, 1, 0)
(4) 标签选择器(div)、伪元素(::before):权重 (0, 0, 0, 1)
这里有一个很多人会忽略的细节——这四个层级之间不存在”进位”关系。也就是说,无论写多少个 class 选择器,都无法超越一个 ID 选择器。
下面这段代码,你能说出文字是什么颜色吗?
/* 特定性: (0, 0, 11, 0) — 11 个 class */
.a.b.c.d.e.f.g.h.i.j.k {
color: blue;
}
/* 特定性: (0, 1, 0, 0) — 1 个 ID */
#target {
color: red;
}
<div id="target" class="a b c d e f g h i j k">什么颜色?</div>
答案是红色。即使堆了 11 个 class,(0, 0, 11, 0) 仍然低于 (0, 1, 0, 0)。因为 b 层级(ID)和 c 层级(class)之间不存在进位——它们是独立比较的。
换句话说,特定性比较的逻辑是:先比 a,a 相同再比 b,b 相同再比 c,c 相同再比 d。只要高层级胜出,低层级的数字完全无关。
再看一个实际开发中常见的场景:
/* 选择器 A:特定性 (0, 1, 1, 1) */
#sidebar .nav-item a {
color: gray;
}
/* 选择器 B:特定性 (0, 0, 2, 1) */
.sidebar-active .nav-item a {
color: blue;
}
想用选择器 B 覆盖 A?做不到——因为 A 有一个 ID 选择器,B 只有 class。很多人的本能反应是加 !important,但这只会让问题更难追踪。
问题的关键在于——应该降低选择器 A 的特定性,而不是提高选择器 B 的。 把 #sidebar 换成 .sidebar,问题就解决了。
四、层叠顺序:特定性相同时,谁写在后面谁赢
特定性解决的是”不同选择器的优先级”问题。但如果两个选择器的特定性完全一致呢?
这时遵循层叠顺序(Cascade Order)规则:在同一来源中,后出现的规则覆盖先出现的。
.button {
color: blue;
}
.button {
color: red;
}
文字是红色,因为第二条规则后出现。
但”同一来源”这个前提很重要。CSS 的层叠来源优先级从低到高是:
(1) 用户代理样式表(浏览器默认样式)
(2) 用户样式表
(3) 作者样式表(开发者写的 CSS)
(4) 作者样式表中的 !important
(5) 用户样式表中的 !important
(6) 用户代理样式表中的 !important
说白了,!important 会反转来源优先级。这就是为什么用户代理的 !important 是最高优先级——浏览器需要确保某些无障碍相关的样式不被开发者覆盖。
五、@layer:2022 年层叠规则的最大变化
2022 年,CSS Cascade Level 5 引入了 @layer(层叠层),这是层叠机制自诞生以来最大的一次扩展。
@layer 解决了什么问题?考虑一个常见场景:项目同时使用 Tailwind CSS 和 Ant Design,两者的样式经常冲突。传统做法是靠特定性”比谁的选择器更长”来覆盖,结果是选择器越写越复杂,维护成本直线上升。
@layer 的思路完全不同——它让开发者显式声明样式的优先级顺序,而且这个顺序独立于特定性之外。
/* 声明层叠层顺序:越靠后优先级越高 */
@layer reset, base, components, utilities;
@layer reset {
* {
margin: 0;
padding: 0;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
}
}
@layer components {
.button {
padding: 8px 16px;
background: #1890ff;
color: white;
}
}
@layer utilities {
.mt-4 {
margin-top: 1rem;
}
}
在这个例子中,utilities 层的样式会覆盖 components 层,即使 components 层的选择器特定性更高。
这里有一个很多人会忽略的细节——未放入任何 @layer 的样式,优先级高于所有 @layer 内的样式。
@layer base {
.title {
color: blue;
} /* 在 layer 内 */
}
.title {
color: red;
} /* 未放入 layer */
.title 的颜色是红色。不在 layer 内的样式 > 所有 layer 内的样式,无论 layer 声明顺序如何。这个规则的设计意图是:让开发者自己写的样式始终能覆盖框架/库的样式(只要框架把样式放在 layer 里)。
换句话说,@layer 给 CSS 层叠增加了一个新的判定层级:
!important > 非 layer 样式 > @layer(按声明顺序,后者优先)> 用户代理样式
实际项目中,Tailwind CSS v3.4+ 和 Ant Design 5.x 都已支持 @layer。一个典型配置:
@layer tailwind-base, antd, tailwind-utilities;
@layer tailwind-base {
@tailwind base;
}
/* Ant Design 样式自动注入到 antd layer */
@tailwind components;
@tailwind utilities;
这样 Tailwind 的 utilities 层优先级最高,antd 的组件样式在中间,Tailwind base 在最低层——不再需要靠 !important 或复杂选择器来解决冲突。
六、:is() 和 :where():特定性的”语法糖”
CSS 4 引入的 :is() 和 :where() 伪类在功能上是等价的——都是”选择器分组”的简写。但它们的特定性行为完全不同:
:is() 的特定性等于其参数中最高的那个选择器。
:where() 的特定性永远为 0。
/* :is() — 特定性取 #id 的值,即 (0, 1, 0, 0) */
:is(#id, .class) {
color: red;
}
/* :where() — 特定性为 (0, 0, 0, 0),相当于没有 */
:where(#id, .class) {
color: blue;
}
/* 普通 class — 特定性 (0, 0, 1, 0) */
.class {
color: green;
}
对一个同时有 id 和 class 的元素,颜色是红色(:is() 的特定性最高)。对只有 .class 的元素,颜色是绿色——因为 .class 的特定性 (0, 0, 1, 0) 高于 :where() 的 (0, 0, 0, 0)。
说白了,:where() 是一个”零特定性包装器”。当你需要写一条可以被任何选择器轻松覆盖的规则时,用 :where() 包裹就行。这在编写 CSS 框架和 reset 样式时特别有用。
七、总结
CSS 样式的生效逻辑可以归结为四步判定:
(1) 层叠来源:!important > 作者样式 > 用户样式 > 浏览器默认
(2) @layer 顺序:非 layer > 后声明的 layer > 先声明的 layer
(3) 特定性:行内 > ID > class/属性/伪类 > 标签/伪元素
(4) 书写顺序:后出现的覆盖先出现的
如果你只记住一句话:CSS 的”优先级”不是一个数字,而是一个多级判定链。 搞清楚每一级的规则,才能真正理解”为什么我的样式不生效”。
延伸阅读:
本系列其他文章:
- 下一篇:规划中
相关主题:
- 如果你对 @layer 在实际项目中引发的兼容性问题感兴趣,可以看:一个 @layer 引发的样式崩塌:企微内置浏览器兼容性排查实录