CSS 权重(Specificity)
很多同学学 CSS 时,最容易冒出的三个疑问是:
- 为什么我写了
.title,却没能覆盖#app h2? - 为什么我把选择器写得很长,结果还是没生效?
- 为什么有时候
!important一出来,前面的“权重计算”像是突然失效了?
这些问题都和 Specificity(权重) 有关。
但一定要先记住一句话:
权重不是 CSS 决胜的第一步,它只是层叠比较中的其中一关。
也就是说,只有当前面的比较都打平了,浏览器才会开始看 Specificity。
一、什么是 CSS 权重
所谓 CSS 权重,本质上是在回答一个问题:
当多条选择器都命中同一个元素的同一个属性时,哪条选择器“更具体”?
你可以把它理解成“描述目标的精准程度”:
- 只写
p,范围很大,不够具体 - 写
.article p,更具体一点 - 写
#app .article p,就更明确了
所以权重比较的不是:
- 选择器谁写得更长
- 谁的字符更多
- 谁看起来更复杂
而是比较:
- 有没有内联样式
- 有几个 ID
- 有几个类、属性、伪类
- 有几个标签、伪元素
Specificity 只在“同一元素、同一属性、同一优先级赛道”里比较。
如果某条声明根本没有命中元素,或者已经在 !important / @layer 阶段输了,那它根本走不到“比权重”这一步。
二、权重到底怎么算
为了便于教学,通常把权重记成四段:
A-B-C-D
其中:
A:内联样式(style="...")B:ID 选择器(如#app)C:类选择器、属性选择器、伪类(如.card、[type="text"]、:hover)D:标签选择器、伪元素(如div、h1、::before)
2.1 哪些会计入权重
| 类型 | 会不会计入 | 示例 |
|---|---|---|
| 内联样式 | 会 | style="color: red" |
| ID 选择器 | 会 | #root |
| 类选择器 | 会 | .btn |
| 属性选择器 | 会 | [disabled]、[type="email"] |
| 伪类 | 会 | :hover、:focus、:nth-child(2) |
| 标签选择器 | 会 | button、section |
| 伪元素 | 会 | ::before、::marker |
2.2 哪些不计入权重
| 类型 | 会不会计入 | 示例 |
|---|---|---|
| 通配符 | 不会 | * |
| 组合符 | 不会 | 、>、+、~ |
:where() 本身及其参数 | 不会 | :where(.card .title) |
这里最容易误解的是组合符。
比如下面两个选择器:
.card .title
.card > .title
它们的权重是一样的,都是 0-0-2-0。因为空格和 > 只是说明“关系”,本身不加分。
2.3 一个非常实用的速记表
你可以先这样记:
- 内联样式:最高一档
- ID:第二档
- 类 / 属性 / 伪类:第三档
- 标签 / 伪元素:第四档
并且比较时是 从左到右逐列比较,不是简单做加法。
三、手把手算几个典型例子
3.1 基础例子
| 选择器 | 权重 | 说明 |
|---|---|---|
p | 0-0-0-1 | 1 个标签 |
h1::before | 0-0-0-2 | 1 个标签 + 1 个伪元素 |
.title | 0-0-1-0 | 1 个类 |
[disabled] | 0-0-1-0 | 1 个属性选择器 |
.card .title:hover | 0-0-3-0 | 2 个类 + 1 个伪类 |
#app | 0-1-0-0 | 1 个 ID |
#app .card h2 | 0-1-1-1 | 1 个 ID + 1 个类 + 1 个标签 |
3.2 看一个完整例子
<div id="app">
<h2 class="title hot">CSS 权重</h2>
</div>
.title.hot {
color: #f97316;
}
#app h2 {
color: #2563eb;
}
两条规则都命中了同一个 h2 的 color。
.title.hot的权重是0-0-2-0#app h2的权重是0-1-0-1
比较时先看第二列:
- 前者有
0个 ID - 后者有
1个 ID
因此 #app h2 直接获胜。
这也说明:
再多的 class,也不能“进位”打败 1 个 ID。
3.3 权重相同怎么办
.nav a {
color: #64748b;
}
.menu a {
color: #0f172a;
}
如果某个链接同时位于 .nav 和 .menu 里,那么这两条规则的权重都是 0-0-1-1。
这时浏览器会继续看:
谁写在后面,谁赢。
所以在权重打平时,书写顺序才会成为最终决定因素。
四、比较规则的关键:逐列比较,不要“十进制思维”
很多同学会把权重误以为是“总分制”,比如:
0-0-10-0看起来很大0-1-0-0看起来也很大
然后就开始纠结到底谁更大。
正确做法不是把它们转成一个总数,而是按列比较:
- 先比
A(内联) - 再比
B(ID) - 再比
C(类 / 属性 / 伪类) - 最后比
D(标签 / 伪元素)
只要某一列先分出高低,后面的列就不用再看了。
例如:
0-1-0-0会赢0-0-999-9990-0-2-0会赢0-0-1-100
因为比较规则是“逐列淘汰”,不是“累计总分”。
五、现代 CSS 里几个很容易考的特殊规则
5.1 :is():取参数列表里“最高”的那个
:is(section, .card, #app) h2
:is() 自己不单独加权重,它会取参数里权重最高的那个。
参数中:
section→0-0-0-1.card→0-0-1-0#app→0-1-0-0
所以 :is(section, .card, #app) 这一段按 #app 算。
整个选择器最终是:
#app→0-1-0-0h2→0-0-0-1
合起来是 0-1-0-1。
5.2 :not():自己不加分,参数照样参与计算
button:not(.disabled)
这个选择器的权重是:
button→0-0-0-1.disabled→0-0-1-0
合起来是 0-0-1-1。
要注意::not() 不是“零权重容器”,它的参数依然会算进去。
5.3 :has():同样取参数里最高的那个
.card:has(img.cover)
可以拆成两部分:
.card→0-0-1-0img.cover→0-0-1-1
所以总权重是 0-0-2-1。
5.4 :where():永远是 0 权重
:where(.article-content) h2
这里 :where(.article-content) 不贡献任何权重,只剩 h2 的 0-0-0-1。
这在工程里非常有价值,因为它可以让“默认样式”更容易被覆盖。
例如:
:where(.prose) h2 {
margin-top: 2em;
}
.page h2 {
margin-top: 1em;
}
第二条规则很容易覆盖第一条,因为第一条故意把自己的权重压低了。
:where()组件库、重置样式、通用排版样式,最怕的就是“默认规则太强,使用方很难覆盖”。
:where() 的价值就在这里:功能照样有,权重却可以保持很低。
六、最容易混淆的几个问题
6.1 !important 不是权重的一部分
这是面试和实战里都特别容易说错的一点。
!important 的作用不是“把 Specificity 加高”,而是把声明放进更高优先级的比较阶段。
也就是说:
- 普通声明之间,才主要比较权重
- 一旦出现
!important,先比较“重要性” - 只有重要性相同,才继续比较权重
所以不要再说“我加了 !important,所以权重更高”。
正确说法应该是:
我加了
!important,所以它进入了更高优先级的比较阶段。
6.2 继承值不会和直接命中的规则拼权重
看下面这个例子:
<div id="app">
<p>这是一段文字</p>
</div>
#app {
color: crimson;
}
p {
color: steelblue;
}
很多同学会误以为:
#app有 ID,权重高- 所以
p应该继承红色
其实最终 p 会是蓝色。
原因是:
#app { color: crimson; }是给父元素自己的声明p { color: steelblue; }是直接命中p的声明
对子元素来说,只要自己拿到了直接声明,就不会再去拿继承值和它拼权重。
所以你要记住:
继承是“没有直接值时的回退”,不是“和直接命中规则抢冠军”。
6.3 内联样式通常很强,但也不是“绝对无敌”
<h2 style="color: tomato;">标题</h2>
内联样式通常可以记成 1-0-0-0,它比普通选择器都更强。
但如果另一条规则进入了更高的层叠优先级,比如相关的 !important 场景,那么仍然需要回到完整的层叠规则里判断,而不是只盯着 Specificity。
七、实战里怎么避免“权重大战”
如果你经常写出下面这种选择器:
.page .layout .sidebar .menu .item a.active
那大概率说明样式架构已经开始变得难维护了。
更好的思路通常是:
7.1 优先使用语义清晰的类名
.menu-link {}
.menu-link-active {}
这通常比不断叠加祖先选择器更稳定,也更容易复用。
7.2 少用 ID 作为样式选择器
ID 权重很高,一旦用了,后续覆盖成本会迅速上升。
ID 更适合:
- 锚点定位
- JS 获取节点
- 无障碍关联
而不太适合作为日常样式覆盖工具。
7.3 给基础样式“降权重”
下面这种写法在工程里很实用:
:where(.article) a {
color: #2563eb;
}
这样使用方只需要很轻量的规则,就能覆盖默认样式。
7.4 用 @layer 管理优先级,而不是一路抬权重
大型项目里,很多样式冲突本质上不是“选择器不够狠”,而是“样式职责没有分层”。
例如可以按这种思路分:
- reset 层
- base 层
- components 层
- utilities 层
这样很多问题可以在“层”的阶段解决,而不是等到最后靠 Specificity 硬拼。
7.5 养成用浏览器 DevTools 排查的习惯
排查样式不生效时,可以按下面顺序看:
- 这条规则有没有命中元素?
- 是不是被
!important或更高层覆盖了? - 如果都在同一赛道里,谁的权重更高?
- 如果权重相同,谁写在后面?
这比“盲目再加一层类名”有效得多。
八、把权重真正学会的一个总结
你可以把 Specificity 浓缩成下面 5 句话:
- 权重只在同一优先级赛道里比较。
- 比较的是具体程度,不是选择器长度。
- 比较顺序是内联 → ID → 类/属性/伪类 → 标签/伪元素。
- 逐列比较,不做总分换算。
- 权重打平时,后写的规则获胜。
如果这 5 句你已经能脱口而出,说明这块已经掌握得很扎实了。
九、面试高频问答
Q1:CSS Specificity 是什么?
参考答案:
Specificity 是 CSS 在层叠比较中的“权重”机制,用来判断当多条选择器同时命中同一个元素的同一个属性时,哪条规则更具体、应该优先生效。它不是 CSS 决策的第一步,通常是在来源、重要性和层叠层比较之后才参与。
Q2:CSS 权重怎么计算?
参考答案:
通常记成四段:A-B-C-D。
A:内联样式B:ID 选择器个数C:类、属性选择器、伪类个数D:标签、伪元素个数
比较时按列从左到右比较,不是把四段加总成一个分数。
Q3:10 个 class 能打过 1 个 ID 吗?
参考答案:
不能。因为比较是逐列进行的,只要 ID 那一列先赢了,后面的 class 再多也没有机会翻盘。所以 0-1-0-0 会赢 0-0-999-999。
Q4:!important 属于 Specificity 吗?
参考答案:
不属于。!important 影响的是层叠中的“重要性”阶段,它会把声明提升到更高优先级的比较赛道。只有在重要性相同的情况下,才继续比较 Specificity。
Q5::is()、:not()、:has()、:where() 的权重规则分别是什么?
参考答案:
:is():取参数列表中权重最高的那个:not():自己不加分,但参数会参与计算:has():和:is()类似,取参数里权重最高的那个:where():始终是 0 权重
其中最常考、最实用的是 :where(),因为它特别适合写“容易被覆盖”的基础样式。
Q6:继承过来的样式和直接命中的样式,谁优先?
参考答案:
直接命中的样式优先。继承值只是在当前元素没有拿到直接值时才起作用,它不会和直接命中的规则去拼 Specificity。
Q7:为什么工程中不建议滥用高权重选择器?
参考答案:
因为高权重会让后续覆盖越来越困难,最后形成“权重大战”,导致样式体系难维护、难复用、难扩展。更好的做法是控制选择器层级、优先使用类名、必要时使用 :where() 和 @layer 做架构治理。
Q8:如果样式没生效,面试时应该怎么排查?
参考答案:
可以按这个顺序回答:
- 先看规则是否真的命中了目标元素
- 再看有没有被
!important或更高层覆盖 - 然后比较 Specificity
- 如果权重相同,再比较书写顺序
这套顺序能说明你理解的不是死记硬背,而是完整的层叠机制。