【每日一面】选择器权重与层叠上下文
权重是四元组逐位比较、低位永不进位,层叠上下文决定 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。
问:项目里样式优先级越写越乱,有什么系统性治理手段?
答:有四个层次治理
-
选择器收敛(采用 BEM 扁平命名,权重稳定在单 class 量级)。
-
:where() 把 reset 与第三方覆盖归零。
-
@layer 把重置、基础、组件、工具类分层,层序即优先级。
-
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 > user !important > author !important"]
B -- 否 --> D["普通来源优先级(高→低)<br/>author > user > UA"]
C --> E{"来源是否相同?"}
D --> E
E -- 否 --> F["来源高者胜"]
E -- 是 --> G{"是否在同一 @layer?<br/>(含未分层)"}
G -- 否 --> H{"普通还是 !important?"}
H -- 普通 --> I["层序:后层 > 前层<br/>未分层 > 所有分层"]
H -- !important --> J["层序反转:前层 > 后层<br/>所有分层 > 未分层"]
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 失效的原因
-
没有生效资格。 z-index 只对定位元素(relative / absolute / fixed / sticky)和 flex / grid 直接子项生效。
position: static写z-index: 9999会被浏览器忽略,根本没有参与比较的资格。 -
被祖先隔离。 祖先创建了层叠上下文(opacity、transform、filter、isolation 等),子元素的 z-index 再大也只在祖先内部比较,祖先整体在外部竞争中输了,内部值再大也无济于事。
-
和上下文整体比较。 两个元素比层级,比的是各自所属上下文的 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 零权重基建。
/* 层序声明:后面的层优先级整体更高,与权重无关 */
@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 意外创建层叠上下文。
/* 现象: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 与父级背景的边界。
/* 情形 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 化。
: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> 最终是什么颜色?怎么让它变红?
<p id="tip" class="text muted" style="color: red">...</p>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 的按钮被盖住了”,给一套可复用的排查路径。
答:四步。
-
确认元素是定位元素或 flex / grid 直接子项,否则 z-index 没参与比较。
-
DevTools 逐级向上排查,重点看 opacity、transform、filter、will-change、isolation、position: fixed 等,命中即被隔离。
-
找到双方各自所属的上下文,比较的是上下文的 z-index,不是按钮的 9999。
-
提升所属上下文 z-index、组件根加
isolation: isolate、或 portal 到 body 从根上下文竞争。
在整体排查中,合理的使用 Layers 面板可以加速排查过程。