做前端时你一定遇到过:明明给弹窗写了 z-index: 9999,它却被一个 z-index: 1 的遮罩压在下面;或者 position: fixed 的吸顶导航,一旦被父元素加了 transform 就再也定不住了。这些”灵异”现象的 root cause 几乎都指向同一个概念:层叠上下文(Stacking Context)。本文用可复现的实战案例,把 z-index 的生效规则、创建条件与排错方法一次讲透。
一、什么是层叠上下文
浏览器在渲染页面时,会把元素按”谁在上面、谁在下面”画到屏幕上,这个过程叫层叠(stacking)。绝大多数元素都活在同一个根层叠上下文里,它们的前后关系由文档流、定位和 z-index 决定。但当某些属性被设置后,浏览器会”开窗”创建一个全新的、独立的小世界——这就是层叠上下文。一旦进入新上下文,里面的元素就只和”自己人”比层级,永远无法直接和外界比 z-index。
理解这一点是解开所有 z-index 谜题的钥匙:z-index 不是全局排行榜,而是同层叠上下文内部的小组排名。
二、z-index 为什么”有时不生效”
2.1 根层叠上下文
页面最顶层有一个根层叠上下文,由 <html> 承载。没有创建新上下文的元素,都在这里按规则排序:定位元素(position 非 static)且设了 z-index 的,会盖在普通元素之上;z-index 数值越大越靠上。
2.2 z-index 只在”同上下文”内比较
这是最容易踩的坑。看下面这个结构:
.parent { position: relative; z-index: 1; }
.child { position: relative; z-index: 9999; }
.sibling{ position: relative; z-index: 2; }
HTML:
<div class="parent">
<div class="child">我 z-index 很大</div>
</div>
<div class="sibling">我是兄弟</div>
直觉上 child(9999)应该压住 sibling(2),但实际 sibling 在上面。原因:parent 的 z-index:1 让它成为新层叠上下文,child 的 9999 只在这个上下文里有效;而 sibling 在根上下文里是 2,根上下文整体(parent=1)低于 sibling=2,所以整组 parent 都被 sibling 压住了。把 parent 的 z-index 去掉或提到 2 以上即可。
2.3 一个最小复现实验
如果你还想更直观地确认”层级只在房间内比较”,可以把上面三段代码直接放进一个 HTML 文件打开:给 parent 加 z-index:1 时 child 被压;把 parent 的 z-index 删掉(或设为 auto),child 立刻翻盘盖住 sibling。这个开关式的对比,能帮你彻底建立”上下文优先于数值”的直觉,比死记规则管用得多。
<!-- 开关对比:注释/取消 .parent 的 z-index 看 child 是否被压 -->
<style>
.parent{position:relative;/* z-index:1; */} /* 取消注释即开窗 */
.child{position:relative;z-index:9999;}
.sibling{position:relative;z-index:2;}
</style>
三、哪些属性会创建新的层叠上下文(速查表)
| 触发条件 | 常见场景 |
|---|---|
| 根元素 <html> | 天然的根层叠上下文 |
| position 非 static 且 z-index 非 auto | 最常用来”开窗” |
| opacity 小于 1 | 半透明弹层、过渡动画 |
| transform / translate / scale / rotate 非 none | 吸顶、动画、居中 |
| filter 非 none | 毛玻璃、模糊 |
| will-change 含上述属性 | 性能优化hint |
| isolation: isolate | 主动隔离层级 |
| position: fixed / sticky | 现代浏览器直接创建 |
| mix-blend-mode / mask / perspective 非 none | 混合、遮罩 |
记住一句话:只要元素”看起来”在做图层相关的变换,它多半已经开了新层叠上下文。这正是 fixed 失效的元凶。
四、实战:position:fixed 被父级 transform 截胡
经典场景:想让一个弹窗相对视口固定,却写在了一个带 transform 动画的容器里,结果弹窗跟着容器跑、甚至定位错乱。
/* 错误:.modal 的 fixed 参照物变成了 .card 而非视口 */
.card { transform: translateY(0); }
.modal { position: fixed; top: 50%; left: 50%; }
修复办法有三个:把弹窗移出带 transform 的容器(推荐,用 React Portal / Vue Teleport 挂到 body);或去掉父级不必要的 transform;或改用 position: absolute 并自行计算定位。涉及层叠与渲染性能时,建议配合 前端性能优化 一起看。
五、实战:opacity / transform 导致的层级错乱
有时只是给某层加了 opacity: 0.99 做淡入,整个模块的层级就”塌”了,因为它被单独关进新上下文,原本和兄弟层比较的 z-index 失效。
.toast { opacity: 0.99; position: fixed; z-index: 100; } /* 开了新上下文 */
.overlay { position: fixed; z-index: 50; } /* 在根上下文 */
/* toast 的 100 只在自己上下文内有效,但 overlay 在根上下文是 50,
而 toast 所在容器的 z-index 若低于 overlay 容器,就会被压住 */
排查时优先怀疑:是不是加了 opacity/filter/transform?把临时动画属性去掉验证,是定位问题最快的方式。
六、isolation: isolate 主动隔离层级
与其纠结 z-index 数字军备竞赛,不如用 isolation: isolate 给元素开一个干净的新层叠上下文,把内部的层级斗争锁死在内部,不再污染外部。
.dropdown { isolation: isolate; position: relative; z-index: 1; }
.dropdown-menu { position: absolute; z-index: 10; } /* 只和 dropdown 内部比 */
这比盲目写 z-index: 9999 优雅得多,也避免了”魔法数字”扩散。无障碍方面,层级清晰的浮层也应保证键盘可达,参见 前端无障碍实战。
七、实战:下拉菜单 / 弹窗被遮挡的排错清单
1) 看父链:从元素向上找,任一祖先有 opacity<1 / transform / filter / will-change 都会开窗
2) 比上下文:确认被遮挡元素和遮挡元素是否在"同一个"层叠上下文
3) 别只看 z-index:比较的是两个元素各自所属上下文的"祖先 z-index"
4) 用 DevTools:Elements > 右侧 Computed 看 stacking context 提示
5) 兜底:用 Portal/Teleport 挂到 body,isolation: isolate 隔离
| 现象 | 最可能原因 | 修复 |
|---|---|---|
| fixed 不定在视口 | 祖先 transform/filter | 移出容器 / 去掉 transform |
| 大 z-index 仍被压 | 所在上下文祖先层级低 | 提升祖先 z-index 或移出 |
| 动画后层级错乱 | opacity/transform 开窗 | isolation: isolate |
| 兄弟元素顺序诡异 | 同上下文内后写盖前写 | 显式 z-index 或调 DOM 序 |
八、调试技巧:DevTools 怎么看层叠
Chrome DevTools 的 Elements 面板里,选中元素看 Computed 子面板,会有一行 “Stacking context: yes/no” 提示;Performance 面板的 Layer 视图能直接看到浏览器把页面切成了哪些合成层。配合 浏览器事件循环 理解渲染流水线,能更系统地定位”为什么这层没上去”。
九、与 3D 变换、will-change 的边界
transform: translateZ(0) 或 will-change: transform 常被用作”开启 GPU 加速”的 hack,但它们同样会创建层叠上下文。这意味着为了性能加的这行代码,可能悄悄改了你的层级关系。结论是:性能 hint 和层级语义要分开管理,不要为了加速而牺牲可预测的层叠。
十、落地清单(Checklist)
| 动作 | 目的 |
|---|---|
| 浮层挂到 body(Portal/Teleport) | 避免祖先 transform 干扰 |
| 用 isolation: isolate 替代魔法数字 | 隔离层级、可读可维护 |
| 只比较同一上下文内的 z-index | 理解排名规则 |
| 动画结束移除 opacity/transform | 避免长期开窗 |
| DevTools 确认 stacking context | 快速定位错乱 |
层叠上下文并不玄学:它就是一个”层级小房间”。只要记住z-index 只在同一房间内比大小、而 position/opacity/transform/filter 都会开门,绝大多数 fixed 失效、弹窗被遮、大数值压不住的问题都能在五分钟内定位。把本文的清单贴进团队规范,下次再遇到,照着排一遍即可。



