z-index设了9999,下拉菜单还是被下方内容盖住——这大概是前端开发中被问到最多的问题之一。要彻底解决这类层叠问题,光会调数值是不够的,得先弄清楚CSS的定位机制和层叠上下文的运作规则。本文从position属性讲起,逐步拆解层叠顺序的判定逻辑,并针对下拉菜单这一典型场景给出完整的排查与修复方案。

一、z-index生效的前提:position属性
很多人一遇到遮挡问题就直接加z-index,结果发现毫无作用。首先要明确一点:z-index只对定位元素有效,也就是position值为relative、absolute、fixed或sticky的元素。对于默认的static元素,z-index会被直接忽略。
下面这段代码是下拉菜单的经典结构,导航项使用relative作为定位锚点,下拉面板使用absolute脱离文档流:
<style>
.nav-item {
position: relative; /* 锚点 */
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 10; /* 生效,因为它是定位元素 */
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
}
</style>
<nav>
<div class="nav-item">
产品
<ul class="dropdown">
<li>功能介绍</li>
<li>价格方案</li>
</ul>
</div>
</nav>这里有个容易忽略的细节:如果只给.dropdown设置position: absolute而忘了写z-index,它依然可能被后续的定位元素覆盖。因为当z-index都为auto时,浏览器按照DOM顺序绘制,后出现的元素画在上面。所以合理做法是:下拉面板显式给一个正值z-index,而可能遮挡它的内容区域要么不定位,要么给一个更低的z-index。
二、层叠上下文:z-index失效的真正元凶
如果说position是z-index的开关,那层叠上下文就是决定比较范围的关键。层叠上下文可以理解为一个独立的“图层组”,组内元素的z-index只在组内比较,组与组之间由根元素或组创建者的z-index决定。换句话说,子元素的z-index再大,也逃不出父层叠上下文的约束。
创建层叠上下文的常见情形有以下几种,排查问题时逐一对照即可:
- 根元素html本身就是一个层叠上下文
- position为absolute或fixed,且z-index不为auto
- position为relative或sticky,且z-index不为auto
- 设置了transform、filter、perspective、will-change,值不为none
- opacity小于1
- 设置了isolation: isolate
- flex或grid容器的子项,且z-index不为auto
看一个典型的“翻车”案例:父元素加了动画用的transform,导致下拉菜单被盖住。
<style>
.header {
transform: translateZ(0); /* 创建了层叠上下文! */
z-index: 1;
}
.main {
position: relative;
z-index: 2;
}
</style>
<header class="header">
<ul class="dropdown" style="position:absolute; z-index:9999;">...</ul>
</header>
<main class="main">内容区</main>乍一看dropdown的9999远大于main的2,应该浮在上面。但实际上header因为transform创建了层叠上下文,整个header(包括里面的9999)被压缩成一个z-index为1的图层,与main的2比较时输了,下拉菜单自然被盖住。解决办法有两种:要么去掉header上的transform,要么把header的z-index提升到比main大的值,比如999。
三、层叠顺序规则与实战排查
同一个层叠上下文内部,元素的绘制顺序遵循固定的七层规则,从低到高依次是:
- 背景和边框(层叠上下文根元素自身)
- 负z-index的子层叠上下文
- 块级元素的普通流
- 浮动元素
- 行内元素
- z-index为auto或0的定位元素
- 正z-index的定位元素(数值越大越靠上)
理解这个顺序能解释很多怪现象。比如一个负z-index的伪元素装饰为什么跑到父元素背景后面去了,或者为什么行内文字能盖住未定位的块级背景。日常排查时建议按下面的步骤来:
第一步,确认被遮挡元素的position不是static;第二步,打开浏览器开发者工具,从下拉菜单向上逐层检查祖先元素,看谁创建了层叠上下文(Chrome会在Computed面板的Layer信息中提示,也可以直接找transform、filter、opacity这些属性);第三步,找到与遮挡元素共同的那个“比较基准”,也就是两者最近的层叠上下文祖先,调整这两个祖先的z-index关系,而不是一味加大子元素的数值。
另外一个实用技巧是给可能出现弹层的组件容器统一使用isolation: isolate,主动创建一个独立的层叠上下文,把组件内部的z-index混乱限制在组件内部,避免污染全局的层叠秩序。对于模态框这类需要覆盖全页的弹层,更稳妥的做法是直接挂载到body下(可借助Portal方式),让它与页面内容处于同一层叠上下文中比较,从根源上规避嵌套约束。
总结一下:z-index失效几乎都不是数值不够大,而是比较对象错了。先确认position,再找层叠上下文的边界,最后在正确的层级上调整数值,三层排查下来,绝大多数遮挡问题都能迎刃而解。