层叠上下文:z-index 为什么不起作用
给元素写了 z-index: 9999 却还是被盖住,是 CSS 里最常见的困惑之一。原因几乎总是:它和遮挡它的元素不在同一个层叠上下文里。
层叠上下文是什么
可以把层叠上下文理解成一个独立的三维空间。在这个空间内部,子元素按 z-index 排序;但对外的整体层级,由这个空间的创建者决定。
也就是说,子元素的 z-index 再大,也超不出父级所在的层。
谁会创建层叠上下文
除了根元素,以下情况都会创建新的层叠上下文:
position为relative/absolute且z-index不为auto;position: fixed或sticky(现代浏览器);opacity小于 1;transform、filter、perspective不为none;will-change指定了上述属性;isolation: isolate;contain: paint/layout。
最后一条经常被忽略:给卡片加一个 transform: translateY(-2px) 的 hover 效果,它就变成了新的层叠上下文,里面的弹层再也盖不住外面的元素。
一个典型场景
<div class="card" style="transform: translateY(0)">
<div class="dropdown" style="z-index: 9999"></div>
</div>
<div class="mask" style="z-index: 10"></div>
dropdown 的 9999 只在 card 内部有效。card 本身的层级是 auto(按文档流),所以 mask 会盖住整个 card,包括 dropdown。
解决办法是把弹层挂到 body 下(Portal 模式),或者不要给 card 创建层叠上下文。
调试方法
在 DevTools 里选中元素,看 Computed 面板的 z-index 与 position,再逐级向上检查祖先是否有 transform、opacity、filter。Chrome 的 Layers 面板可以直观看到图层划分。
实践建议
- 建立一套固定的 z-index 阶梯(如 10 内容、100 悬浮、1000 弹层、2000 提示),不要随手写 9999;
- 弹层尽量用 Portal 挂到根节点;
- 慎用
transform做纯视觉微调,它会带来层叠副作用; - 需要隔离时,用
isolation: isolate显式声明意图,比依赖副作用清晰。