约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
浏览器不是把全页所有元素的 z-index 拉通排序,而是先按「上下文」分组:每个层叠上下文是一个独立考场,内部成员的层级只在考场内比,整场考试的最终位置由创建该考场的元素在外层考场里的位置决定。所以子元素的 z-index: 9999 永远逃不出父级上下文的「天花板」——父级在外层只排到 1,子级内部再大也只在父级的框里画。
.overlay { z-index: 1; } /* 外层考场:它在第 1 层 */
.overlay .tip { z-index: 9999; } /* 考场内再大也只是「第 1 层里的最上面 */
.sibling { z-index: 2; position: relative; } /* 外层第 2 层 → 盖住整个 overlay 考场 */同一个层叠上下文内,绘制顺序固定为七层,后面的盖前面的:背景边框 → 负 z-index 子上下文 → 块级盒 → 浮动盒 → 行内盒 → z-index: 0 / auto 的定位元素 → 正 z-index 子上下文。这就是「没有 z-index 时图片盖住文字(行内盒在块级盒之后)」「负 z-index 会沉到背景之上、内容之下」的原因。
position: fixed/sticky(几乎 always)、transform、perspective、filter、backdrop-filter、opacity 小于 1、mix-blend-mode、isolation: isolate、will-change 指定上述属性、contain: layout/paint——都会为元素创建层叠上下文。所谓「加了 transform 动画后 z-index 全乱了」,就是动画开始的一瞬间元素从「无上下文」变成「新上下文」,整个子树被整体锁进了新的考场。
| 属性 | 触发条件 | 典型翻车场景 |
|---|---|---|
| transform | 任何非 none 值 | 入场动画开始瞬间层级重排 |
| opacity | < 1 | 淡入动画时下拉菜单被盖住 |
| filter / backdrop-filter | 非 none | 毛玻璃卡片内的弹层被外部盖住 |
| position: fixed / sticky | 存在即创建 | fixed 头部内的下拉被内容盖住 |
| will-change | 指定 transform 等 | 「只是声明优化」却改了层级 |
| isolation: isolate | 存在即创建 | 混合模式隔离时顺带改了层级 |
查它的父级链上有没有非 static 定位或 transform/opacity/filter 等属性——它们创建了层叠上下文,把元素锁在局部考场里。要比较的其实是考场创建者之间的层级。
transform 非 none 即创建层叠上下文:动画开始的一瞬间元素从「普通盒」变成「新考场」,它整个子树的层级被锁进这个考场内部,与外部元素的相对关系随之改变。
规范上是 32 位有符号整数(2147483647),浏览器会把更大的值钳到该上限。依赖极限数值本身就是坏味道,应改用分层常量管理。