~/ ?.log $
返回文章列表
12 min read
更新于 2026年3月6日

我不知道的 CSS(01)— 选择器匹配、优先级与层叠

一、z-index 设了 9999 为什么还是不生效?

一、z-index 设了 9999 为什么还是不生效?

很多人遇到过这个场景:给元素设了 z-index: 9999,但它就是被另一个元素挡住了。凭直觉,9999 应该够大了吧?

问题往往不出在 z-index 的数字上,而出在层叠上下文选择器优先级上。CSS 的样式最终由谁”胜出”,取决于一套完整的层叠机制——选择器匹配方向、特定性计算、层叠顺序,以及 2022 年新增的 @layer 规则。

这套机制,很多人天天在用,但真正说得清楚的并不多。


二、选择器为什么从右往左匹配?

写下 .container .item span 这个选择器时,直觉上浏览器应该先找 .container,再找 .item,最后找 span——从左到右。

但实际上,浏览器的匹配方向恰好相反:从右到左。

浏览器先找到页面上所有的 span 元素,然后逐个往上检查,看它的祖先中是否存在 .item.item 的祖先中是否存在 .container。只要某一级验证失败,立刻放弃当前 span,检查下一个。

为什么这样做更快?考虑一个有 1000 个 DOM 节点的页面,其中只有 3 个 span。从右到左,只需要对 3 个 span 做祖先链验证。而从左到右,需要先找到所有 .container,再遍历它们的后代,搜索空间大得多。

下面这个例子可以直观感受差异:

<div class="container">
  <div class="item">
    <span>目标</span>
  </div>
  <div class="other">
    <p>非目标</p>
    <div>非目标</div>
    <!-- ...假设这里有 500 个子元素 -->
  </div>
</div>

对选择器 .container .item span,从右到左只需检查 1 个 span 的祖先链(2 次向上查找)。而从左到右,找到 .container 后需要遍历它所有后代,包括 .other 下的 500 个无关节点。

如果你只记住一句话:浏览器匹配选择器是从最右边的”键选择器”开始的,然后逐级向上验证。 这就是为什么把最具体的选择器放在最右边,能帮助浏览器更快完成匹配。


三、特定性的四级权重:不是简单的数字比大小

选择器匹配之后,下一个问题是:多条规则都命中了同一个元素,谁的样式生效?

答案是特定性(Specificity)。很多人知道”ID 选择器比 class 选择器优先级高”,但具体的计算规则经常被搞混。

特定性的计算基于四个层级,记为 (a, b, c, d)

(1) 行内样式(style="..."):权重 (1, 0, 0, 0)

(2) ID 选择器(#id):权重 (0, 1, 0, 0)

(3) 类选择器(.class)、属性选择器([type="text"])、伪类(:hover):权重 (0, 0, 1, 0)

(4) 标签选择器(div)、伪元素(::before):权重 (0, 0, 0, 1)

这里有一个很多人会忽略的细节——这四个层级之间不存在”进位”关系。也就是说,无论写多少个 class 选择器,都无法超越一个 ID 选择器。

下面这段代码,你能说出文字是什么颜色吗?

/* 特定性: (0, 0, 11, 0) — 11 个 class */
.a.b.c.d.e.f.g.h.i.j.k {
  color: blue;
}

/* 特定性: (0, 1, 0, 0) — 1 个 ID */
#target {
  color: red;
}
<div id="target" class="a b c d e f g h i j k">什么颜色?</div>

答案是红色。即使堆了 11 个 class,(0, 0, 11, 0) 仍然低于 (0, 1, 0, 0)。因为 b 层级(ID)和 c 层级(class)之间不存在进位——它们是独立比较的。

换句话说,特定性比较的逻辑是:先比 aa 相同再比 bb 相同再比 cc 相同再比 d。只要高层级胜出,低层级的数字完全无关。

再看一个实际开发中常见的场景:

/* 选择器 A:特定性 (0, 1, 1, 1) */
#sidebar .nav-item a {
  color: gray;
}

/* 选择器 B:特定性 (0, 0, 2, 1) */
.sidebar-active .nav-item a {
  color: blue;
}

想用选择器 B 覆盖 A?做不到——因为 A 有一个 ID 选择器,B 只有 class。很多人的本能反应是加 !important,但这只会让问题更难追踪。

问题的关键在于——应该降低选择器 A 的特定性,而不是提高选择器 B 的。#sidebar 换成 .sidebar,问题就解决了。


四、层叠顺序:特定性相同时,谁写在后面谁赢

特定性解决的是”不同选择器的优先级”问题。但如果两个选择器的特定性完全一致呢?

这时遵循层叠顺序(Cascade Order)规则:在同一来源中,后出现的规则覆盖先出现的。

.button {
  color: blue;
}
.button {
  color: red;
}

文字是红色,因为第二条规则后出现。

但”同一来源”这个前提很重要。CSS 的层叠来源优先级从低到高是:

(1) 用户代理样式表(浏览器默认样式)

(2) 用户样式表

(3) 作者样式表(开发者写的 CSS)

(4) 作者样式表中的 !important

(5) 用户样式表中的 !important

(6) 用户代理样式表中的 !important

说白了,!important 会反转来源优先级。这就是为什么用户代理的 !important 是最高优先级——浏览器需要确保某些无障碍相关的样式不被开发者覆盖。


五、@layer:2022 年层叠规则的最大变化

2022 年,CSS Cascade Level 5 引入了 @layer(层叠层),这是层叠机制自诞生以来最大的一次扩展。

@layer 解决了什么问题?考虑一个常见场景:项目同时使用 Tailwind CSS 和 Ant Design,两者的样式经常冲突。传统做法是靠特定性”比谁的选择器更长”来覆盖,结果是选择器越写越复杂,维护成本直线上升。

@layer 的思路完全不同——它让开发者显式声明样式的优先级顺序,而且这个顺序独立于特定性之外

/* 声明层叠层顺序:越靠后优先级越高 */
@layer reset, base, components, utilities;

@layer reset {
  * {
    margin: 0;
    padding: 0;
  }
}

@layer base {
  body {
    font-family: system-ui, sans-serif;
  }
}

@layer components {
  .button {
    padding: 8px 16px;
    background: #1890ff;
    color: white;
  }
}

@layer utilities {
  .mt-4 {
    margin-top: 1rem;
  }
}

在这个例子中,utilities 层的样式会覆盖 components 层,即使 components 层的选择器特定性更高。

这里有一个很多人会忽略的细节——未放入任何 @layer 的样式,优先级高于所有 @layer 内的样式

@layer base {
  .title {
    color: blue;
  } /* 在 layer 内 */
}

.title {
  color: red;
} /* 未放入 layer */

.title 的颜色是红色。不在 layer 内的样式 > 所有 layer 内的样式,无论 layer 声明顺序如何。这个规则的设计意图是:让开发者自己写的样式始终能覆盖框架/库的样式(只要框架把样式放在 layer 里)。

换句话说,@layer 给 CSS 层叠增加了一个新的判定层级:

!important > 非 layer 样式 > @layer(按声明顺序,后者优先)> 用户代理样式

实际项目中,Tailwind CSS v3.4+ 和 Ant Design 5.x 都已支持 @layer。一个典型配置:

@layer tailwind-base, antd, tailwind-utilities;

@layer tailwind-base {
  @tailwind base;
}

/* Ant Design 样式自动注入到 antd layer */

@tailwind components;
@tailwind utilities;

这样 Tailwind 的 utilities 层优先级最高,antd 的组件样式在中间,Tailwind base 在最低层——不再需要靠 !important 或复杂选择器来解决冲突。


六、:is() 和 :where():特定性的”语法糖”

CSS 4 引入的 :is():where() 伪类在功能上是等价的——都是”选择器分组”的简写。但它们的特定性行为完全不同:

:is() 的特定性等于其参数中最高的那个选择器。

:where() 的特定性永远为 0。

/* :is() — 特定性取 #id 的值,即 (0, 1, 0, 0) */
:is(#id, .class) {
  color: red;
}

/* :where() — 特定性为 (0, 0, 0, 0),相当于没有 */
:where(#id, .class) {
  color: blue;
}

/* 普通 class — 特定性 (0, 0, 1, 0) */
.class {
  color: green;
}

对一个同时有 idclass 的元素,颜色是红色(:is() 的特定性最高)。对只有 .class 的元素,颜色是绿色——因为 .class 的特定性 (0, 0, 1, 0) 高于 :where()(0, 0, 0, 0)

说白了,:where() 是一个”零特定性包装器”。当你需要写一条可以被任何选择器轻松覆盖的规则时,用 :where() 包裹就行。这在编写 CSS 框架和 reset 样式时特别有用。


七、总结

CSS 样式的生效逻辑可以归结为四步判定:

(1) 层叠来源:!important > 作者样式 > 用户样式 > 浏览器默认

(2) @layer 顺序:非 layer > 后声明的 layer > 先声明的 layer

(3) 特定性:行内 > ID > class/属性/伪类 > 标签/伪元素

(4) 书写顺序:后出现的覆盖先出现的

如果你只记住一句话:CSS 的”优先级”不是一个数字,而是一个多级判定链。 搞清楚每一级的规则,才能真正理解”为什么我的样式不生效”。


延伸阅读:


本系列其他文章:

  • 下一篇:规划中

相关主题:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;