搞懂层叠上下文,你就能解释「为什么 z-index 写了 9999 还是被盖住」,并且能在五分钟内定位这类层级问题。
第一步:先记住绘制顺序
在同一个层叠上下文里,浏览器按固定顺序绘制(从下到上):
- 该上下文元素的背景与边框
z-index 为负的子元素
- 块级盒子(普通流)
- 浮动元素
- 行内/行内块元素
z-index: auto 或 0 的定位元素
z-index 为正的定位元素
关键点:z-index 只在同一个层叠上下文内部比较。跨上下文比大小,等于拿两个房间的门牌号比谁住得高。
第二步:认清哪些属性会「顺手」创建层叠上下文
除了根元素 <html>,以下都会创建新的层叠上下文:
/* 1. 定位 + 有数值的 z-index(auto 不算) */
.box { position: relative; z-index: 1; }
/* 2. fixed / sticky 无论有没有 z-index */
.box { position: fixed; }
/* 3. 透明度小于 1 */
.box { opacity: .99; }
/* 4. 变形、滤镜、混合 */
.box { transform: translateZ(0); }
.box { filter: blur(0); }
.box { mix-blend-mode: multiply; }
/* 5. 组合隔离、容器约束 */
.box { isolation: isolate; }
.box { contain: paint; }
/* 6. flex / grid 子项,z-index 不为 auto 时 */
.item { z-index: 1; } /* 父级是 display:flex 就够了,不用 position */
/* 7. 预告层叠上下文(不创建,但会被某些浏览器提前处理) */
.box { will-change: transform; }
第三步:复现一个典型坑
<div class="mask">遮罩</div>
<div class="card">
<span class="tag">标签</span>
</div>
.mask { position: fixed; inset: 0; z-index: 10; background: rgba(0,0,0,.5); }
.card { opacity: .99; } /* 元凶:创建了层叠上下文 */
.tag { position: absolute; z-index: 9999; }
.tag 写多大都没用,因为它的「天花板」是 .card。.card 整体在 z-index:10 的遮罩之下,子元素自然出不来。
注意:opacity、transform、filter 这三个属性最容易在动画和过渡里被无意加上,调试时先查祖先链上有没有它们。
第四步:三种修法
修法一:把遮挡方降到同一层叠上下文里比较。
.card { position: relative; z-index: 20; } /* 去掉 opacity */
.mask { z-index: 10; }
修法二:用 Portal / 挂到 body。 弹窗、下拉菜单、Tooltip 这类组件,直接把 DOM 渲染到 body 末尾,绕开所有祖先上下文。React 用 createPortal,Vue 用 <Teleport to="body">。
修法三:用 isolation: isolate 主动封层。 组件库中给每个卡片显式隔离,就再也不会被外部 z-index 意外渗透:
.card { isolation: isolate; }
第五步:负 z-index 与 flex 的额外坑
/* 坑:负 z-index 会跑到父元素背景之下,甚至看不见 */
.child { position: relative; z-index: -1; }
负 z-index 元素排在「上下文元素的背景」之上、但排在父元素背景之下。如果父元素有不透明背景,子元素直接消失。解决办法是给父元素加 isolation: isolate,让子元素的负值只在父元素内部生效。
另一个反直觉点:flex / grid 子项的 z-index 不需要 position 就能生效,因为它自动带定位语义。很多人在普通块级元素上写 z-index 不生效,就是因为漏了 position。
第六步:怎么快速定位问题
打开 DevTools,在元素面板里逐层向上选中祖先,看 Computed 面板:
- Chrome / Edge:Layers 面板可以看层叠树,ThreeD 视图能可视化层叠关系
- Firefox:元素检查器会在创建了层叠上下文的元素旁标注
- 通用做法:临时给可疑祖先加
outline: 1px solid red,确认自己是落在哪个上下文里
调试顺序建议固定成三步:查自身有没有 position、查祖先链有没有创建上下文、最后才怀疑 z-index 数值。
小结
z-index 只在同一个层叠上下文内比较,跨上下文无效
opacity < 1、transform、filter、will-change、position: fixed/sticky、z-index 数值都会创建层叠上下文
- 祖先一旦创建上下文,子元素的
z-index 就被封顶
- 弹窗类组件优先挂到
body,组件内部用 isolation: isolate 主动隔离
- flex / grid 子项
z-index 生效不需要 position
- 负
z-index 会被父元素背景遮住,用 isolation 收拢
- 排查顺序:自身定位 → 祖先上下文 → 数值大小