跳到主要内容

CSS 权重(Specificity)

很多同学学 CSS 时,最容易冒出的三个疑问是:

  1. 为什么我写了 .title,却没能覆盖 #app h2
  2. 为什么我把选择器写得很长,结果还是没生效?
  3. 为什么有时候 !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:标签选择器、伪元素(如 divh1::before

2.1 哪些会计入权重

类型会不会计入示例
内联样式style="color: red"
ID 选择器#root
类选择器.btn
属性选择器[disabled][type="email"]
伪类:hover:focus:nth-child(2)
标签选择器buttonsection
伪元素::before::marker

2.2 哪些不计入权重

类型会不会计入示例
通配符不会*
组合符不会 >+~
:where() 本身及其参数不会:where(.card .title)

这里最容易误解的是组合符。

比如下面两个选择器:

.card .title
.card > .title

它们的权重是一样的,都是 0-0-2-0。因为空格和 > 只是说明“关系”,本身不加分。

2.3 一个非常实用的速记表

你可以先这样记:

  1. 内联样式:最高一档
  2. ID:第二档
  3. 类 / 属性 / 伪类:第三档
  4. 标签 / 伪元素:第四档

并且比较时是 从左到右逐列比较,不是简单做加法。


三、手把手算几个典型例子

3.1 基础例子

选择器权重说明
p0-0-0-11 个标签
h1::before0-0-0-21 个标签 + 1 个伪元素
.title0-0-1-01 个类
[disabled]0-0-1-01 个属性选择器
.card .title:hover0-0-3-02 个类 + 1 个伪类
#app0-1-0-01 个 ID
#app .card h20-1-1-11 个 ID + 1 个类 + 1 个标签

3.2 看一个完整例子

<div id="app">
<h2 class="title hot">CSS 权重</h2>
</div>
.title.hot {
color: #f97316;
}

#app h2 {
color: #2563eb;
}

两条规则都命中了同一个 h2color

  • .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 看起来也很大

然后就开始纠结到底谁更大。

正确做法不是把它们转成一个总数,而是按列比较:

  1. 先比 A(内联)
  2. 再比 B(ID)
  3. 再比 C(类 / 属性 / 伪类)
  4. 最后比 D(标签 / 伪元素)

只要某一列先分出高低,后面的列就不用再看了。

例如:

  • 0-1-0-0 会赢 0-0-999-999
  • 0-0-2-0 会赢 0-0-1-100

因为比较规则是“逐列淘汰”,不是“累计总分”。


五、现代 CSS 里几个很容易考的特殊规则

5.1 :is():取参数列表里“最高”的那个

:is(section, .card, #app) h2

:is() 自己不单独加权重,它会取参数里权重最高的那个。

参数中:

  • section0-0-0-1
  • .card0-0-1-0
  • #app0-1-0-0

所以 :is(section, .card, #app) 这一段按 #app 算。

整个选择器最终是:

  • #app0-1-0-0
  • h20-0-0-1

合起来是 0-1-0-1

5.2 :not():自己不加分,参数照样参与计算

button:not(.disabled)

这个选择器的权重是:

  • button0-0-0-1
  • .disabled0-0-1-0

合起来是 0-0-1-1

要注意::not() 不是“零权重容器”,它的参数依然会算进去。

5.3 :has():同样取参数里最高的那个

.card:has(img.cover)

可以拆成两部分:

  • .card0-0-1-0
  • img.cover0-0-1-1

所以总权重是 0-0-2-1

5.4 :where():永远是 0 权重

:where(.article-content) h2

这里 :where(.article-content) 不贡献任何权重,只剩 h20-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 排查的习惯

排查样式不生效时,可以按下面顺序看:

  1. 这条规则有没有命中元素?
  2. 是不是被 !important 或更高层覆盖了?
  3. 如果都在同一赛道里,谁的权重更高?
  4. 如果权重相同,谁写在后面?

这比“盲目再加一层类名”有效得多。


八、把权重真正学会的一个总结

你可以把 Specificity 浓缩成下面 5 句话:

  1. 权重只在同一优先级赛道里比较。
  2. 比较的是具体程度,不是选择器长度。
  3. 比较顺序是内联 → ID → 类/属性/伪类 → 标签/伪元素。
  4. 逐列比较,不做总分换算。
  5. 权重打平时,后写的规则获胜。

如果这 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:如果样式没生效,面试时应该怎么排查?

参考答案

可以按这个顺序回答:

  1. 先看规则是否真的命中了目标元素
  2. 再看有没有被 !important 或更高层覆盖
  3. 然后比较 Specificity
  4. 如果权重相同,再比较书写顺序

这套顺序能说明你理解的不是死记硬背,而是完整的层叠机制。