特异性计算
基于 CSS 现代标准 · 核于 2026-06
速查
- 特异性写成三列
ID-CLASS-TYPE(也记作a-b-c),每条选择器算出一组三元值 - ID 列:
#id选择器,每个+1(1-0-0) - CLASS 列:类
.x、属性[attr]、伪类:hover,每个+1(0-1-0) - TYPE 列:类型
p、伪元素::before,每个+1(0-0-1) - 加
0:通用*、组合器> + ~空格、:where()、嵌套& :is()/:not()/:has()自身不加权,取参数里最高特异性那一项- 比较「逐列从左往右」:左列大者直接赢,不是把三列加成一个总分——1 个 ID 压过 11 个类
- 行内
style=""概念上更高(记作1-0-0-0),只有!important能压它 !important不算特异性,属于「来源/重要性」维度,比特异性更先决出(见 层叠与继承)- 平局(同来源、同层、同特异性)才看书写顺序,后写的赢
特异性是什么,又不是什么
当多条规则给同一元素的同一属性赋了值、且它们处于同一来源、同一层叠层时,浏览器用特异性这把尺子量「哪条选择器更具体」,越具体越优先。
要先破一个常见误解:特异性不是一个十进制数,而是三列计分。把它想成版本号那样的三元组 (ID, CLASS, TYPE),比较时从最高位逐列往下比,高位一旦更大就直接定胜负——这意味着进位是不存在的,10 个类也凑不出 1 个 ID。
三列权重表
| 选择器种类 | 计入哪一列 | 单个权重 |
|---|---|---|
ID 选择器 #main | ID(最高位) | 1-0-0 |
类 .btn、属性 [type="x"]、伪类 :hover | CLASS(中位) | 0-1-0 |
类型 div、伪元素 ::before | TYPE(最低位) | 0-0-1 |
通用 *、组合器 > + ~ 空格、:where()、& | 不计 | 0-0-0 |
几个容易记错的归类
- 属性选择器算 CLASS 列(
0-1-0),不是 TYPE——[id="main"]是属性选择器,权重只有0-1-0,远低于真正的 ID 选择器#main(1-0-0); - 伪元素(
::before)算 TYPE 列(0-0-1),和元素同级,别和单冒号伪类(CLASS 列)搞混; :where()永远是0-0-0,是唯一一个「吞掉自身参数权重」的伪类。
一步步算给你看
逐个选择器累加各列。以这条复合选择器为例:
<a class="btn primary" href="#">链接</a>a {
} /* 0-0-1 ← 类型 a */
a.btn {
} /* 0-1-1 ← 加类 .btn */
a.btn.primary {
} /* 0-2-1 ← 再加类 .primary */
a.btn.primary[href] {
} /* 0-3-1 ← 加属性 [href] */
a.btn.primary[href]:hover {
} /* 0-4-1 ← 加伪类 :hover */可见类、属性、伪类都进 CLASS 列累加,类型进 TYPE 列。这条 0-4-1 的选择器已经相当难覆盖了。
逐列比较:为什么 1 个 ID 压过一堆类
比较两条选择器时,先比 ID 列,相等再比 CLASS 列,再相等才比 TYPE 列:
#title {
color: green;
} /* 1-0-0 */
.page .section .box [data-id] {
color: orange;
} /* 0-4-0 */尽管右边堆了 4 个类/属性,它的 ID 列是 0,而 #title 的 ID 列是 1——第一列就分出胜负,标题是绿色。这正是「特异性不会进位」的直接后果,也是「少用 ID、多用类」建议的根源:ID 一旦参战,普通类几乎无法翻盘。
再看一个 ID 列打平、靠 CLASS 列决胜的例子:
#app [data-id="title"] {
color: green;
} /* 1-1-0 */
#title {
color: orange;
} /* 1-0-0 */两边 ID 列都是 1,比 CLASS 列:1 > 0,绿色胜出。
函数式伪类对特异性的影响
这是现代 CSS 的关键考点,四个伪类各有脾气:
/* :where() —— 永远 0-0-0,吞掉参数权重 */
:where(#sidebar .item) a {
/* 整个 :where(...) 记 0;只有末尾的 a 计 0-0-1 → 合计 0-0-1 */
}
/* :is() —— 取参数里最高特异性 */
:is(p) {
} /* 0-0-1(来自 p) */
:is(p, #id) {
} /* 1-0-0(参数里 #id 最高) */
/* :not() —— 自身不加权,但算参数最高 */
p:not(#id) {
} /* 1-0-1(p 的 0-0-1 + #id 的 1-0-0) */
/* :has() —— 同 :is(),取参数最高 */
h1:has(+ h2, > #id) {
} /* 1-0-1(h1 的 0-0-1 + 参数里 #id 最高 1-0-0) */:is() / :not() / :has() 会被参数里的「权重炸弹」抬高
只要参数列表里混进一个高权重选择器(尤其是 ID),整个伪类的特异性就被拉到那个高度。比如想写一个「低特异性的排除规则」却写成 :not(#legacy),结果平白多出 1-0-0,反而更难被覆盖。需要可控的低特异性时,把它们包进 :where(),或避免在参数里使用 ID。
行内样式与 !important:在特异性之外
有两样东西不参与上面的三列计分,但实际优先级更高:
行内 style
写在元素 style="" 里的声明,概念上可看作多出一列、记作 1-0-0-0——它压过任何选择器写出的样式,无论后者特异性多高:
<p style="color: purple;">这段文字是紫色的,选择器很难覆盖</p>#x.y.z p {
color: green; /* 1-2-1,仍然输给行内样式 */
}!important
!important 根本不在特异性这个维度——它把声明提升到层叠算法里更高的「重要性 / 来源」档位,比特异性更早被裁决(见 层叠与继承 的四步顺序)。所以一条特异性极低、但带 !important 的声明,能压过特异性极高的普通声明:
a {
color: red !important;
} /* 0-0-1 但带 !important → 赢 */
#nav .menu a.link {
color: blue;
} /* 1-2-1 普通声明 → 输 */唯一能压住行内 !important 的,是用户 / 浏览器来源的 !important,以及 CSS 过渡(transition)——这些都属于「来源 + 重要性」层面,下一页细讲。
两条 !important 之间,才轮到特异性
当两条声明都带 !important、且同来源同层时,它们重新回到「比特异性」的赛道,高者胜。!important 不是免死金牌,它只是把战场换到了更高的档位。
平局时:书写顺序兜底
如果两条声明来源相同、层叠层相同、特异性也完全相等,那么——后写的那条赢。这就是为什么覆盖一个组件库的同特异性样式,常常只要保证你的 CSS 在它之后加载即可:
.btn {
color: blue;
}
.btn {
color: green; /* 同为 0-1-0,后写 → 最终是绿色 */
}同一声明块内,同名属性也是后者覆盖前者,这正是渐进增强写两遍的原理:
.box {
color: rgb(0 128 0); /* 老浏览器用这条 */
color: oklch(0.6 0.15 150); /* 认识的浏览器用这条覆盖 */
}实战:如何「赢得」特异性而不滥用 !important
想提高一条规则的优先级,比堆 !important 更克制的手法:
/* 重复类名,温和地 +1 个 CLASS 权重(0-1-0 → 0-2-0) */
.btn.btn {
color: green;
}
/* 或用属性写法复述,效果类似 */
.btn[class] {
color: green;
}但更治本的做法是从架构上消解特异性战争——把第三方 / 工具类样式装进较低的 @layer,让自己的普通样式天然胜出(下下页 级联层实战),以及用 :where() 给基础样式归零特异性。
小结
特异性是「逐列比较、永不进位」的三列计分:ID ≫ 类/属性/伪类 ≫ 类型/伪元素,* 与组合器不计,:where() 归零,:is() / :not() / :has() 取参数最高。但请记住——特异性只是层叠四步中的第三步。在它之前,还有「来源」和「重要性」两道更先决的关卡。下一页把完整的层叠顺序与继承补齐:层叠与继承。