【每日一面】选择器权重与层叠上下文

2026-10-10 26 min 9095 字 -- 次阅读
摘要

权重是四元组逐位比较、低位永不进位,层叠上下文决定 z 轴绘制顺序;z-index 失效要么没有生效资格,要么被创建上下文的祖先隔离,排查要沿祖先链查起。

z-index 写到 9999,弹窗还是被轮播图盖住,你怎么排查?

这道题前半段考层叠上下文,后半段考排查思路,背后则是 CSS 优先级的考察,同一元素的多条声明听谁的,z 轴上谁绘制在上面。

优先级权重口诀面试八股都会背,ID 大于 class、class 大于元素,但是 11 个 class 顶不顶得过 1 个 ID、两个 !important 撞车怎么分胜负、transform 为什么会悄悄改变层级格局,这些细节才是能力的区分。

基础问答

问:CSS 权重怎么计算?

答:四级从高到低,行内、ID、类选择器(含伪类、属性)、元素(含伪元素),可以记作四元组 (a, b, c, d),从 a 开始逐位比较,高位分出胜负就不再看低位。#nav .item:hover 是 (0,1,2,0),.card .title span 是 (0,0,2,1),前者赢在 ID 位。

问:继承来的样式有权重吗?为什么 * { color: #333 } 会把继承的文字颜色干掉?

答:继承的声明不参与子元素的选择器级联比较,可以认为比通配符 * 还低。body { color: #666 } 是 body 自己的声明,子元素拿到的是继承值;* { color: #333 } 对子元素是直接命中,直接命中永远高过继承,全站文字随之变 #333。这也是 Reset 里 * { margin: 0 } 安全、* { color: xxx } 危险的原因。

Reset(CSS Reset) 就是一段放在样式表前面的 CSS,用来重置浏览器默认样式,让不同浏览器、不同项目的初始表现更一致。

问:!important 和行内样式谁大?两个 !important 撞车听谁的?

答:!important 高于一切普通声明,行内也不例外。两个 !important 之间退回同一套规则进行比较,先看来源,author important < user important < user-agent important,与普通声明正好相反,同来源比权重,再相同比源码顺序,后来居上。

CSS 级联规范里的三种样式来源(origin):user-agent、user、author,级联时先比较来源和重要性。

问:权重完全相同,最终样式怎么定?

答:看源码顺序,后声明的优先级更高,这就是覆盖第三方库样式要么提权重、要么放在库后面的原理。不过更官方一些的答案是 @layer 级联层,层与层的优先级由声明顺序决定,与权重无关。

问:什么是层叠上下文?哪些操作会创建它?

答:层叠上下文是一个三维概念,决定了元素沿 z 轴(指向用户的轴)的堆叠顺序。<html> 是根层叠上下文,所有其他层叠上下文都嵌套在它内部。上下文内部自成体系:子元素的 z-index 只在内部比较;整个上下文又作为整体与兄弟比层级。

根据规范,一个元素只要满足以下任一条件,就会创建层叠上下文:

  • 文档根元素 <html>。

  • position 为 absolute 或 relative,且 z-index 不为 auto。

  • position 为 fixed 或 sticky(无论 z-index 为何值)。

  • flex 容器的子元素,且 z-index 不为 auto。

  • grid 容器的子元素,且 z-index 不为 auto。

  • opacity 小于 1。

  • mix-blend-mode 不为 normal。

  • isolation 为 isolate。

  • will-change 指定了任意一个会创建层叠上下文的属性。

  • contain 为 layout、paint 或包含它们的合成值。

  • container-type 为 size 或 inline-size。

问:项目里样式优先级越写越乱,有什么系统性治理手段?

答:有四个层次治理

  1. 选择器收敛(采用 BEM 扁平命名,权重稳定在单 class 量级)。

  2. :where() 把 reset 与第三方覆盖归零。

  3. @layer 把重置、基础、组件、工具类分层,层序即优先级。

  4. z-index 用 CSS 变量 token 化,禁掉 9999 这类魔法数字。

扩展延伸

权重机制

权重的常见误区是把它当成可进位的数:ID 是 100、class 是 10、元素是 1,加起来比大小。这个模型会翻车的,比如按百进制换算,11 个 class 的 110 能赢 1 个 ID 的 100,但是实际是不会的,它也无法解释高位定胜负的比较规则。

正确的模型是逐位比较:a 位行内、b 位 ID、c 位类/伪类/属性、d 位元素/伪元素。先比 a 位,分出胜负立即停止,依此类推。可以认为位与位之间的差距是无穷大,低位再多也无法进位。

选择器权重说明
#app #main .title(0,2,1,0)两个 ID 压倒一切 class 组合
.card .title span(0,0,2,1)两个 class 加一个元素
div.card:hover::after(0,0,2,2):hover 计 c 位、::after 计 d 位
*、>、+、~、空格等组合器(0,0,0,0)通配符与组合器零权重

权重只在判断链的一个环节起作用。同一元素同一属性的多条竞争声明比较顺序如下:

flowchart TD
    A["同一元素、同一属性<br/>多条声明竞争"] --> B{"是否带 !important?"}

    B -- 是 --> C["!important 来源优先级(高→低)<br/>UA !important &gt; user !important &gt; author !important"]
    B -- 否 --> D["普通来源优先级(高→低)<br/>author &gt; user &gt; UA"]

    C --> E{"来源是否相同?"}
    D --> E

    E -- 否 --> F["来源高者胜"]
    E -- 是 --> G{"是否在同一 @layer?<br/>(含未分层)"}

    G -- 否 --> H{"普通还是 !important?"}
    H -- 普通 --> I["层序:后层 &gt; 前层<br/>未分层 &gt; 所有分层"]
    H -- !important --> J["层序反转:前层 &gt; 后层<br/>所有分层 &gt; 未分层"]

    G -- 是 --> K["比较特异性 (a,b,c,d)"]
    I --> K
    J --> K

    K --> L{"特异性是否相同?"}
    L -- 否 --> M["特异性高者胜"]
    L -- 是 --> N["源码顺序:后声明胜"]

    style F fill:#e8f5e9,stroke:#1a7a3a,stroke-width:2px
    style M fill:#e8f5e9,stroke:#1a7a3a,stroke-width:2px
    style N fill:#e8f5e9,stroke:#1a7a3a,stroke-width:2px

比较链前两级的来源与重要性互为镜像:

优先级普通声明important 声明
高author 样式(页面 CSS)UA样式
中user 样式(浏览器设置)user 样式
低UA 样式author 样式
  • 来源反转的设计意图

    普通声明里 author 样式最强,页面可以按设计稿呈现,一旦都加 !important,user 与 user-agent 样式会更优先,比如视觉无障碍用户把系统设成高对比度,这样的诉求是不应该被任何页面的 author 剥夺的。所以用 !important 去压制用户偏好,在规范层面上就是不可行的。

  • @layer

    在 @layer 的层序比较面前,选择器特异性再高也没用。只要两条声明属于不同的层,且都是同一来源、同一重要性、普通声明,那么系统先比较的是层序,其次是特异性。层序高的层直接杀死比赛,根本不会进入特异性比较。

    值得注意的是,以上只在同一来源、同一重要性、普通声明时成立。一旦带 !important,层序会出现反转,声明得更早的层反而更高;而普通声明里未分层(没有用 @layer 包裹的)高于所有分层,!important 里未分层低于所有分层。

    @layer 声明列表中声明得更晚的层,就是层序比较高的。可以理解为盖楼房,盖的22层,那怎么都会比1层高。

层叠上下文

权重解决的是同一个属性听谁的,层叠上下文解决的是z 轴上谁绘制在上面。将两套规则混为一谈是面试最常见的扣分点。

七层绘制顺序

每个层叠上下文内部,元素按固定的七层顺序绘制:

flowchart TD
    L1["① 层叠上下文根自身的背景与边框"]
    L2["② 负 z-index 的定位子元素"]
    L3["③ 常规流块级盒子"]
    L4["④ 浮动元素"]
    L5["⑤ 行内内容"]
    L6["⑥ z-index 为 auto / 0 的定位元素"]
    L7["⑦ 正 z-index 的定位子元素"]
    L1 --> L2 --> L3 --> L4 --> L5 --> L6 --> L7

    style L1 fill:#f5f5f5,stroke:#999
    style L7 fill:#e8f0fe,stroke:#0a84ff,stroke-width:2px

越晚绘制越靠上。可以这么去记,背景打底、负值沉底、文档流居中、行内在上、定位封顶。下面是两个常被追问的推论:

  • 浮动元素在第 ④ 层,常规流块级背景在第 ③ 层,所以浮动盖住块级背景。但行内内容在第 ⑤ 层,所以文字画在浮动元素之上,这就是为什么浮动不遮住文字形成文字环绕的原因。

  • z-index: -1 沉在第 ② 层,位于上下文根背景(第 ① 层)之上、常规流内容(第 ③ 层)之下,可做沉到文字后面的装饰。

z-index 失效的原因

  1. 没有生效资格。 z-index 只对定位元素(relative / absolute / fixed / sticky)和 flex / grid 直接子项生效。position: static 写 z-index: 9999 会被浏览器忽略,根本没有参与比较的资格。

  2. 被祖先隔离。 祖先创建了层叠上下文(opacity、transform、filter、isolation 等),子元素的 z-index 再大也只在祖先内部比较,祖先整体在外部竞争中输了,内部值再大也无济于事。

  3. 和上下文整体比较。 两个元素比层级,比的是各自所属上下文的 z-index。假设轮播图模块 z-index: 2,弹窗所在的卡片 z-index: 1,并且各自都创建了层叠上下文,在这里的卡片整体输给轮播图,弹窗内部写 9999 也跟着降下去。

层叠上下文 ≠ BFC

两概念触发条件有重叠,却是两套独立机制,BFC 管理的是块级盒子的布局边界(浮动不外溢、margin 不折叠),层叠上下文管理的则是 z 轴绘制层级。overflow: hidden 只创建 BFC,opacity: 0.5 只创建层叠上下文,z-index: 0 的定位元素都会创建,所以下次看到元素被遮盖就写 overflow: hidden,并不一定会解决问题。

层级治理

  • token 分级:z-index 收敛为少量 CSS 变量(见示例 4),魔法数字禁入。

  • 主动隔离:组件根节点加 isolation: isolate 封闭内部层级,组件库的标准做法。

  • 结构规避:弹层、Toast 用 portal 渲染到 body 下,跳出业务祖先链,从根上下文竞争。

调试用 DevTools 的 Layers 面板,按绘制层级列出全部层,快速定位创建者。

权重侧的治理原则考虑全项目选择器贴近 (0,1,0) 一个量级,比较交给源码顺序与层序,而非 ID 与嵌套深度。采用 BEM 将块、元素、修饰符全落在单个 class 上,永不嵌套。覆盖第三方库样式可以采取以下手段:

手段原理副作用
选择器自重复(.btn.btn.btn)c 位 +2,硬抬权重脆弱,库选择器一变就失效
@layer 把库样式放进低层层序碾压,权重不再相关要求能控制库样式的层归属
:where(库选择器) 重写归零后任意选择器自然覆盖需照抄库选择器,有同步成本

三者的演进方向一致:从比数字走向声明层序。

隐藏在优先级之外的比较

在权重与层叠上下文之外,还有一个隐藏的变量,就是继承。

  • 可继承属性:集中在排版类color、font 系列、line-height、text-align 等。

  • 默认不继承:主要是盒模型与布局类,如 width、margin、padding、background。

我们大多数时候可以这么判断,跟文字有关的往下传,跟盒子有关的不传。不过具体的还是要以规范为主,比如visibility、cursor 等就无法根据这个来判断。

继承传递的是计算值。比如 line-height: 1.5em 在父元素上算成 30px,子元素继承的是 30px,而 line-height: 1.5 继承的是系数,子元素按自身字号重新算。所以在实践中,我们在写行高的时候推荐写无单位数字。

想要对抗优先级与继承的默认行为,可以依靠级联关键字:

关键字行为对 color 的效果
initial回到规范初始值通常是黑色
inherit强制继承,对不可继承属性也有效继承父级颜色
unset可继承属性当 inherit,其余当 initial等价于 inherit
revert按来源逐级回退通常回到 UA 默认,链接蓝

revert 并不是总要回到浏览器默认样式的,而是可以回退到上一来源,author 里的 revert 回退到 user,没有 user再回退到 UA。

还有一个 Shadow DOM ,这个实现的是选择器隔离,外部选择器默认进不来,shadow tree 内部的选择器也出不去。但它提供几个显式通道:

  • ::part():外部通过 part="..." 指定 shadow 内部元素可被外部样式选中。

  • ::slotted():shadow 内部选择器可以选中被 slot 分发的宿主子元素。

  • CSS 自定义属性:可继承的 --* 变量会穿透边界,是主题化的常用通道。

但要注意:Shadow DOM 不阻止继承。宿主元素上的 color、font、line-height 等可继承属性,仍然会穿透进 shadow tree,被内部元素继承。所以组件默认能继承页面的排版,又不会被页面选择器任意改动内部结构。

代码示例

示例 1:@layer 分层 + :where 零权重基建。

css
/* 层序声明:后面的层优先级整体更高,与权重无关 */
@layer reset, base, components, utilities;

@layer reset {
  :where(ul, ol) { margin: 0; padding: 0; list-style: none; } /* 零权重 */
}

@layer components {
  #app .list .item { display: flex; } /* 权重再高也只在本层内比较 */
}

@layer utilities {
  .hidden { display: none; } /* utilities 声明在后,优先级更高 */
}

示例 2:opacity 意外创建层叠上下文。

css
/* 现象:hover 的 tooltip 被 sibling 盖住 */
.card { position: relative; opacity: 0.99; }          /* 随手加的优化,创建了层叠上下文 */
.card .tooltip { position: absolute; z-index: 9999; } /* 只在 card 内部有效 */
.sibling { position: relative; z-index: 2; }          /* 整体压过 card */

/* 修复:card 加 z-index: 10 整体胜出;或 tooltip portal 到 body 从根上下文竞争 */
.card { position: relative; opacity: 0.99; z-index: 10; }

示例 3:z-index: -1 与父级背景的边界。

css
/* 情形 A:父级不是层叠上下文 */
.wrap { background: #fff; }
.wrap .deco { position: absolute; z-index: -1; }
/* deco 参与外层上下文的第 ② 层,wrap 背景在第 ③ 层,deco 被背景挡住,视觉上消失 */

/* 情形 B:父级是层叠上下文 */
.wrap { background: #fff; position: relative; z-index: 0; }
.wrap .deco { position: absolute; z-index: -1; }
/* deco 沉到 wrap 内部第 ② 层:背景之上、文字之下,背景托底、装饰居中、文字浮顶 */

同一个 z-index: -1,父级是否为层叠上下文,结论完全相反。

示例 4:z-index token 化。

css
:root {
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-modal: 1000;
  --z-toast: 1100;
}
.modal { z-index: var(--z-modal); }
.toast { z-index: var(--z-toast); } /* 新增组件对号入座,禁止自造数字 */

对比

对比维度权重(specificity)层叠上下文(stacking context)
解决的问题同一属性的多条声明谁生效z 轴上多个盒子谁绘制在上面
作用对象CSS 声明DOM 盒子的渲染层级
计算方式四元组 (a,b,c,d) 逐位比较上下文以根元素的层叠级别参与父上下文比较,内部按七层顺序绘制
相同时的裁决同一来源、重要性、层序、特异性下,源码顺序后者赢同层叠级别时,DOM 顺序靠后者绘制在上
典型误区以为 11 个 class 能顶 1 个 ID以为 z-index 值大就一定在上面
现代解法@layer 分层、:where() 归零isolation: isolate、portal、z-index token 化
调试手段DevTools 样式面板看划线Layers 面板、排查祖先链

权重定归属,层叠定高低,两条规则职责不同,组合起来才是 CSS 优先级的全貌。

面试追问

追问1:11 个 class 和 1 个 ID,谁的优先级高?为什么?

答:ID 高。权重不是可进位的十进制数,而是逐位比较的四元组:b 位(ID)与 c 位(class)之间是无穷大的差距,c 位叠到 11 也无法进位。相似的陷阱:元素选择器叠 100 层也赢不过一个 .a,位与位之间没有可比性。

追问2:下面的 <p> 最终是什么颜色?怎么让它变红?

xml
<p id="tip" class="text muted" style="color: red">...</p>
css
p { color: blue !important; }
#tip { color: green; }
.text.muted { color: gray; }

答:蓝色。p { color: blue !important } 是author important 声明,整体优先级高于全部普通声明,行内、ID、双 class 通通出局。想要变红只需将行内升级为 color: red !important,这样一来,同为author important,先比较内联与选择器这个层级,而内联 !important 高于任何选择器 !important。

追问3:给卡片加了 transform: translateZ(0) 做渲染优化,之后卡片内弹出的 tooltip 就被旁边的板块盖住了,为什么?(前提条件position: relative 下)

答:transform 非 none 会创建层叠上下文。tooltip 的 z-index: 9999 从此只在卡片内部有效;卡片自身 z-index 为 auto(第 ⑥ 层),旁边板块 z-index: 1(第 ⑦ 层)整体盖住导致 tooltip 跟着沉没。

修复方案是,给卡片设更高 z-index 让整体在 z 轴更高,或将 tooltip portal 到 body 跳出祖先链。另外需要注意 will-change: transform 同样会创建层叠上下文,并且性能优化属性都要过一遍层叠副作用清单。

追问4::is()、:where()、:has() 的权重怎么算?工程上各自的价值是什么?

答::is()、:has()、:not() 取参数中权重最大者计入;:where() 恒为零。

工程价值上,reset、主题、第三方覆盖用 :where() 包一层,业务侧任何选择器可以自然覆盖,可以避免 !important 滥用;:is() 能够简化复杂选择器的书写,如 :is(header, main, aside) a,但它会抬高权重;:has() 提供父选择器能力,.card:has(img) 按 .card(c 位 1)与 img(d 位 1)合并得 (0,0,1,1)。

追问5:线上反馈“z-index: 9999 的按钮被盖住了”,给一套可复用的排查路径。

答:四步。

  1. 确认元素是定位元素或 flex / grid 直接子项,否则 z-index 没参与比较。

  2. DevTools 逐级向上排查,重点看 opacity、transform、filter、will-change、isolation、position: fixed 等,命中即被隔离。

  3. 找到双方各自所属的上下文,比较的是上下文的 z-index,不是按钮的 9999。

  4. 提升所属上下文 z-index、组件根加 isolation: isolate、或 portal 到 body 从根上下文竞争。

在整体排查中,合理的使用 Layers 面板可以加速排查过程。

评论