先记住这个答案
不能。父元素一旦创建了层叠上下文,它的子元素就只在这个上下文内部按 z-index 排序。和外部元素比较时,参与比较的是父元素这个整体,用的是父元素自己的层级。所以如果父元素 A 的 z-index 是 1,另一个区域 B 是 2,A 里面的子元素写 z-index: 9999 也只能待在 B 的下面。
- 子元素的
z-index只在所在层叠上下文内部比较 - 跨上下文比较的是两个祖先上下文的层级
- 变换与透明度可建层叠上下文
- 修法是提升父级层级,或把元素挪出去(Portal)
一个弹窗被遮住的例子
这是项目里最常见的场景:页面主体里弹出一个遮罩,结果被固定在顶部的导航栏压住了一截。
<style>
.header { position: fixed; top: 0; z-index: 2; }
.main { position: relative; z-index: 1; } /* 创建了层叠上下文 */
.modal { position: fixed; inset: 0; z-index: 9999; }
</style>
<header class="header">导航栏</header>
<main class="main">
<div class="modal">弹窗遮罩</div>
</main>
<!-- 结果:导航栏盖在弹窗上面 -->
<!-- 在根上下文里比较的是 .main(1) 和 .header(2),.modal 的 9999 不参与 -->浏览器的思路是:根层叠上下文里有两个成员,.header 层级 2,.main 层级 1,所以 .header 在上。.modal 属于 .main 这个上下文,它的 9999 只用来和 .main 里的其他子元素比较。把 .main 的 z-index 删掉,或者改成 3,弹窗就能正常盖住导航栏。
怎么判断两个元素谁在上面
可以把层叠上下文想成文件夹。文件夹里的文件按 z-index 排好序,但文件夹放在桌面上时,只看文件夹本身的顺序。一个文件就算在文件夹里排第一,也不可能跑到另一个文件夹上面。
具体判断时,从两个元素分别往上找祖先,直到找到它们共同所在的那个层叠上下文。然后在这个上下文里,比较两边各自的「代表」,也就是直接属于这个上下文的那两个祖先(或元素本身)。代表的层级决定了整组的前后。上面的例子里,共同的上下文是根,两个代表是 .header 和 .main。
如果两个代表的 z-index 相同,就按 DOM 顺序,后出现的在上面。
容易答错的地方
- 以为
z-index是全局比较的 z-index从来不是全局排名,它只在同一个层叠上下文的兄弟之间比较。遇到盖不住的情况,一味往上加数字没有用,要先找到是哪个祖先创建了上下文。- 以为没写
z-index就没有层叠上下文 opacity、transform、filter、position: fixed都会创建。父元素加了一个淡入动画,动画期间opacity小于 1,里面的下拉菜单就可能被别的区域盖住,动画结束又恢复,这种时好时坏的问题最难查。
面试官还会怎么问?
弹窗、下拉菜单这类组件库一般怎么避免这个问题?
把弹层渲染到 body 下面,脱离业务组件的 DOM 层级。React 用 createPortal,Vue 用 Teleport,antd、Element 的弹层默认都挂在 body 上。这样弹层直接处在根层叠上下文里,只需要和其他顶层元素比 z-index。
z-index: auto 和 z-index: 0 有什么区别?
对定位元素来说,auto 不创建层叠上下文,子元素还是参与外层比较;0 会创建层叠上下文,把子元素关在里面。两者在外层的绘制位置相同,都排在层级 0 这一档,区别只在会不会把子元素圈起来。
isolation: isolate 有什么用?
它只做一件事:创建层叠上下文,不附带定位、透明度等副作用。适合在组件根节点上用,把组件内部的 z-index 封起来,避免内部的层级数字和页面其他地方互相打架。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。