CSS 选择器优先级怎么计算?!important 和 @layer 怎么影响层叠?

进阶高频原理约 5 分钟读完

一句话回答

多条规则作用在同一个属性上时,浏览器按层叠顺序依次比较:来源和重要性 → 行内样式 → 层(@layer)→ 选择器优先级 → 出现顺序,前一步分出胜负就不再往下比。优先级按 (ID 数, 类/属性/伪类数, 标签/伪元素数) 从左到右逐位比较,不进位。!important 会把声明提到更高的重要性级别;@layer 里后声明的层优先级更高,但对 !important 声明顺序反过来。

详细解析

层叠的比较顺序

  1. 来源和重要性:浏览器默认样式、用户样式、开发者样式,再叠加是否 !important。普通声明里开发者样式最高;!important 声明的顺序反过来,浏览器默认样式的 !important 最高
  2. 行内样式:style 属性里的声明,赢过同一级别所有选择器写的规则
  3. 层:@layer 决定的先后
  4. 优先级:选择器的 (a, b, c)
  5. 出现顺序:都一样时,后写的覆盖先写的

第 1 步和第 2 步之间,规范里还有一步"封装上下文"(Shadow DOM 内外样式的比较),普通页面可以忽略。

动画和过渡在规范里是两个单独的来源:正在运行的 CSS 动画(@keyframes)的值高于所有普通声明,但低于所有 !important 声明;过渡(transition)中的值高于一切,包括 !important。

优先级 (a, b, c) 怎么算

  • a:ID 选择器个数
  • b:类选择器、属性选择器、伪类的个数
  • c:标签选择器、伪元素的个数
  • 通配符 *、组合符(空格、>、+、~)不计数
选择器 优先级
li (0, 0, 1)
ul li::before (0, 0, 3)
.nav a:hover (0, 2, 1)
input[type="text"] (0, 1, 1)
#header .logo (1, 1, 0)
:is(#a, .b) p (1, 0, 1)
:where(#a, .b) p (0, 0, 1)

比较时从 a 开始,a 大的直接赢。所以 1 个 ID 赢过任意多个类,不存在"11 个类加起来超过 1 个 ID"。

函数式伪类

  • :is()、:not()、:has() 本身不计数,取参数列表里优先级最高的那个
  • :where() 的优先级永远是 0,适合写容易被覆盖的基础样式(比如组件库的默认样式)

!important 和 @layer

@layer 把样式分组,层的先后由第一次声明的顺序决定:

  • 普通声明:后声明的层赢,没有放进任何层的样式赢过所有层
  • !important 声明:顺序完全反转,先声明的层赢,层里的 !important 赢过层外的 !important

这个设计让底层的重置样式、第三方库在必要时能用 !important 守住关键规则,不被上层随手覆盖。层的作用是"先比层、再比优先级":只要层更高,选择器再简单也能赢。

继承不参与优先级

继承来的值没有优先级。只要有任何规则直接选中了元素,哪怕是 *,也会覆盖从父元素继承的值。

代码示例

CSS
/* 先声明层的顺序:reset 最低,components 最高 */
@layer reset, base, components;

@layer components {
  .btn { color: white; }
}

@layer reset {
  /* 层更低,即使用了 ID 选择器,普通声明也输给 components 里的 .btn */
  #app button { color: black; }
}

/* 不在任何层里:普通声明赢过所有层 */
button { background: gray; }
HTML
<div style="color: red">
  <!-- 结果是蓝色:p 规则直接选中了元素,继承来的红色没有优先级 -->
  <p>文字</p>
</div>
<style>
  p { color: blue; }
</style>

面试官可能追问

行内样式和 !important 谁的优先级高?

样式表里的 !important 赢过普通的行内样式,因为重要性比较在先。行内样式加了 !important 后,又赢过样式表里的 !important。想覆盖行内样式,要么改 JS 不再写行内样式,要么在样式表里用 !important。

为什么不推荐滥用 !important?

它破坏了正常的层叠,要覆盖一个 !important 只能再写一个更高的 !important,最后变成军备竞赛,样式很难维护。合理场景是工具类(比如 .hidden)、覆盖无法修改的第三方行内样式。有了 @layer 后,很多原来靠 !important 解决的覆盖问题,可以靠调整层的顺序解决。

第三方 UI 库的样式总是覆盖不掉,可以怎么办?

可以用 @import url(lib.css) layer(vendor); 把它整体放进一个低优先级的层,自己的业务样式放在层外或更高的层,这样不用比拼选择器就能覆盖。另一种思路是提高自己选择器的优先级,比如重复类名 .btn.btn,但不如层清晰。

易错点

  • 优先级是逐位比较,不是加权求和,类再多也比不过一个 ID
  • 层外的普通样式赢过所有层,但层外的 !important 反而输给层里的 !important
  • :where() 优先级为 0,:is() 取参数中最高的,两者写法相同但结果不同

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。