CSS选择器及优先级

13 分钟

为什么只背权重数字不够

很多人记 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 状态管理的替代品。复杂页面里大量使用关系型匹配,可能增加样式匹配成本,也会降低可读性。

级联顺序:完整判断链路

当多个规则命中同一个元素和同一个属性时,可以按下面顺序判断:

  1. 先看声明来源和重要性:普通声明与 !important 声明分开比较。
  2. 再看级联层:如果使用了 @layer,层的顺序会影响结果。
  3. 再看选择器优先级:比较四元组。
  4. 再看作用域距离:现代 CSS 中 @scope 可能影响就近规则。
  5. 最后看声明顺序:同等条件下后声明覆盖先声明。

普通项目中最常遇到的是第 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

面试中如何回答

可以按这个结构回答:

  1. CSS 最终样式由级联规则决定,选择器权重只是其中一部分。
  2. 选择器权重可以用四元组理解:内联、ID、类/属性/伪类、元素/伪元素。
  3. 权重从左到右比较,不做十进制进位。
  4. !important 会提升声明重要性,但重要声明之间仍然比较权重和顺序。
  5. :where() 权重为 0,:is():not():has() 会取参数中的相关权重。
  6. 工程上应避免高权重选择器,优先使用低权重类名和清晰的组件边界。

总结

CSS 优先级不是简单的数字相加,而是一套完整级联机制:

  • 选择器本身决定匹配哪些元素。
  • 优先级四元组决定同来源同层级下谁更强。
  • **!important**改变普通声明和重要声明的比较顺序。
  • 声明顺序在其他条件相同时决定最终结果。
  • 工程可维护性比单次覆盖成功更重要。

写 CSS 时,最好的策略不是把权重写高,而是让权重保持可控。低权重、低嵌套、边界清晰的样式,才更容易长期维护。