CSS选择器及优先级
为什么只背权重数字不够
很多人记 CSS 优先级时会背一套数字:内联样式 1000、ID 100、类 10、元素 1。这个记法能帮助入门,但在真实项目里经常不够用。
原因是浏览器决定一个样式是否生效,不只看选择器权重,还会同时考虑:
- 样式来源:浏览器默认样式、用户样式、开发者样式。
- 是否带
!important:重要声明会改变比较顺序。 - 级联层和作用域:例如
@layer会影响同一来源下的优先级。 - 选择器优先级:ID、类、属性、伪类、元素、伪元素的组合权重。
- 书写顺序:前面都相同时,后声明的样式覆盖先声明的样式。
所以更准确的说法是:CSS 最终生效结果由级联规则决定,选择器优先级只是级联规则中的一环。
常见选择器类型
选择器的作用是找到页面中需要应用样式的元素。常见类型可以按匹配方式理解。
基础选择器
* {
box-sizing: border-box;
}
button {
cursor: pointer;
}
.card {
padding: 16px;
}
#app {
min-height: 100vh;
}
| 选择器 | 示例 | 含义 |
|---|---|---|
| 通配选择器 | * | 匹配所有元素 |
| 元素选择器 | button | 匹配指定标签 |
| 类选择器 | .card | 匹配指定 class |
| ID 选择器 | #app | 匹配指定 id |
工程里最常用的是类选择器。它可复用、语义清晰,也不会像 ID 选择器那样把权重抬得太高。
属性选择器
input[type="text"] {
border: 1px solid #ddd;
}
button[disabled] {
cursor: not-allowed;
}
属性选择器适合根据 DOM 属性匹配元素。它的权重和类选择器同级。
常见场景包括:
- 表单控件状态样式。
- 根据
data-*属性写组件状态样式。 - 对外部生成的 DOM 做定向覆盖。
组合选择器
.article p {
line-height: 1.8;
}
.nav > li {
list-style: none;
}
.title + .desc {
margin-top: 8px;
}
.item ~ .item {
border-top: 1px solid #eee;
}
| 选择器 | 示例 | 含义 |
|---|---|---|
| 后代选择器 | .article p | 匹配内部所有后代 |
| 子代选择器 | .nav > li | 只匹配直接子元素 |
| 相邻兄弟选择器 | .title + .desc | 匹配紧跟其后的兄弟元素 |
| 通用兄弟选择器 | .item ~ .item | 匹配后续兄弟元素 |
组合选择器本身不额外增加权重,真正参与计算的是其中的 ID、类、属性、伪类、元素和伪元素。
伪类和伪元素
a:hover {
color: #1677ff;
}
input:focus {
outline: 2px solid #1677ff;
}
.article::first-line {
font-weight: 600;
}
.button::before {
content: "";
}
伪类描述元素的状态或结构,例如 :hover、:focus、:nth-child()。伪元素描述元素的某个虚拟部分,例如 ::before、::after、::first-line。
权重上:
- 伪类按类选择器计算。
- 伪元素按元素选择器计算。
优先级如何计算
选择器优先级通常写成四元组:
(a, b, c, d)
含义是:
a:内联样式,来自style属性。b:ID 选择器数量。c:类选择器、属性选择器、伪类数量。d:元素选择器、伪元素数量。
比较时从左到右逐位比较,不做十进制进位。
示例一:类选择器和元素选择器
.card p {
color: red;
}
p {
color: blue;
}
.card p 的权重是 (0, 0, 1, 1),p 的权重是 (0, 0, 0, 1),所以 .card p 生效。
示例二:ID 选择器压过多个类选择器
#main {
color: red;
}
.page .content .title {
color: blue;
}
#main 的权重是 (0, 1, 0, 0),.page .content .title 的权重是 (0, 0, 3, 0)。比较时先看 ID 位,1 > 0,所以 ID 选择器胜出。
这也是不建议在业务样式里滥用 ID 的原因:后续想覆盖它,只能继续使用 ID 或 !important,样式会越来越难维护。
示例三:权重相同看声明顺序
.button {
color: red;
}
.button {
color: blue;
}
两个选择器权重相同,后声明的 color: blue 生效。
这也是 CSS 文件顺序、组件样式加载顺序会影响最终效果的原因。
!important 到底有多强
!important 会把普通声明提升为重要声明。
.button {
color: red !important;
}
#submit {
color: blue;
}
即使 #submit 的选择器权重更高,普通声明也无法覆盖重要声明,所以 color: red 生效。
但 !important 不是无敌规则。两个重要声明之间仍然要继续比较来源、级联层、选择器权重和声明顺序。
.button {
color: red !important;
}
.primary.button {
color: blue !important;
}
两者都是重要声明时,.primary.button 权重更高,所以蓝色生效。
工程建议是:不要把 !important 当成常规覆盖手段。它更适合用于非常明确的边界场景,例如工具类、无障碍强制样式、第三方样式兜底覆盖。业务组件里频繁使用 !important,通常说明选择器设计已经失控。
现代选择器里的常见追问
:where() 的权重永远是 0
:where(.article h2) {
margin-top: 24px;
}
:where() 很特殊,它内部写得再复杂,整体权重也按 0 计算。
这对写基础样式很有用。比如组件库或文章内容样式希望提供默认表现,但又希望业务方很容易覆盖,就可以用 :where() 降低权重。
:where(.prose h2) {
font-size: 24px;
}
.article-title {
font-size: 28px;
}
.article-title 可以轻松覆盖前面的默认样式。
:is() 和 :not() 取参数中最高权重
:is(.card, #dialog) .title {
color: red;
}
:is() 自身不单独增加权重,它的权重取参数列表中最高的那一项。上面例子里因为参数包含 #dialog,所以整个选择器会带上 ID 级别的权重。
:not() 也是类似逻辑:
.button:not(.disabled) {
opacity: 1;
}
这里 .disabled 会参与权重计算。
实践中要注意:在 :is() 里混入 ID 选择器,可能会无意中把一整条规则的权重抬高。
:has() 也是按参数权重计算
.card:has(img) {
padding-top: 0;
}
:has() 可以根据子元素或后代元素反向匹配父元素。它的权重也会受参数影响。
:has() 很强,但不要把它当成 JS 状态管理的替代品。复杂页面里大量使用关系型匹配,可能增加样式匹配成本,也会降低可读性。
级联顺序:完整判断链路
当多个规则命中同一个元素和同一个属性时,可以按下面顺序判断:
- 先看声明来源和重要性:普通声明与
!important声明分开比较。 - 再看级联层:如果使用了
@layer,层的顺序会影响结果。 - 再看选择器优先级:比较四元组。
- 再看作用域距离:现代 CSS 中
@scope可能影响就近规则。 - 最后看声明顺序:同等条件下后声明覆盖先声明。
普通项目中最常遇到的是第 3 和第 5 点。但如果项目使用了 CSS Reset、组件库、Tailwind、CSS Modules 或 @layer,就需要把级联层也考虑进去。
工程中如何避免权重失控
优先使用低权重类选择器
.card-title {
font-size: 18px;
font-weight: 600;
}
单类选择器权重稳定,覆盖成本低,是业务样式最推荐的写法。
不推荐写太深的选择器:
.page .content .card .header .title span {
color: red;
}
这类选择器的问题不是不能生效,而是后续很难覆盖,也容易和 DOM 结构强绑定。只要结构调整,样式就可能失效。
少用 ID 写样式
ID 更适合做页面锚点或 JS 查询,不适合作为业务样式的主要选择器。
/* 不推荐 */
#submitButton {
color: white;
}
/* 推荐 */
.submit-button {
color: white;
}
使用类名可以让样式复用,也能降低后续覆盖成本。
控制嵌套深度
在 Sass、Less 或 CSS-in-JS 中,很容易写出过深嵌套:
.page {
.card {
.header {
.title {
color: red;
}
}
}
}
编译后权重会不断升高。一般建议嵌套不超过 2 到 3 层,能用明确类名表达的,就不要依赖祖先结构。
第三方样式覆盖要隔离
覆盖组件库样式时,常见做法是给业务区域加一个局部容器,避免全局污染。
.profile-page .ant-btn-primary {
border-radius: 8px;
}
如果覆盖规则非常多,应该优先考虑组件库提供的主题变量、Token 或 API,而不是堆选择器和 !important。
面试中如何回答
可以按这个结构回答:
- CSS 最终样式由级联规则决定,选择器权重只是其中一部分。
- 选择器权重可以用四元组理解:内联、ID、类/属性/伪类、元素/伪元素。
- 权重从左到右比较,不做十进制进位。
!important会提升声明重要性,但重要声明之间仍然比较权重和顺序。:where()权重为 0,:is()、:not()、:has()会取参数中的相关权重。- 工程上应避免高权重选择器,优先使用低权重类名和清晰的组件边界。
总结
CSS 优先级不是简单的数字相加,而是一套完整级联机制:
- 选择器本身决定匹配哪些元素。
- 优先级四元组决定同来源同层级下谁更强。
- **
!important**改变普通声明和重要声明的比较顺序。 - 声明顺序在其他条件相同时决定最终结果。
- 工程可维护性比单次覆盖成功更重要。
写 CSS 时,最好的策略不是把权重写高,而是让权重保持可控。低权重、低嵌套、边界清晰的样式,才更容易长期维护。