返回博客

z-index 更高却被遮住?层叠上下文隔离坑透底部弹窗

底部弹窗弹出来,用户手指往下一划,发现下面几个选项死活点不到——被一条浮动 tabbar 挡住了。打开调试器一看,van-popup 的 z-index 是 200,tabbar 才 100,按理应该弹窗在上面。这个 bug 让人摸不着头脑,因为数字明明更大。

问题出在 CSS 层叠上下文(Stacking Context)的隔离机制。这不是微信小程序特有的,而是 CSS 规范本身的行为,只是在"容器内弹窗 + 容器外 fixed 导航"的场景里特别容易踩。

现象

页面有一块 position: absolute 的主内容容器,底部弹窗(van-popup)渲染在容器内。页面底部有一个 position: fixed 的悬浮 tabbar,挂在容器外的页面根节点。弹出城市选择、筛选项等底部弹窗时,弹窗底部的内容区域被 tabbar 遮盖,遮盖区域内的选项无法点击。

即便把 van-popup 的 z-index 调到 999,也没有任何效果。

根因

CSS 规定:带有 position(非 static)且 z-index 非 auto 的元素,或带有特定属性(opacity < 1transform 等)的元素,会创建一个独立的层叠上下文。该上下文内的子元素 z-index,只在这个上下文内部参与排序,无法跨越上下文与外部元素直接比较。

坑:z-index 是容器内的局部坐标,不是全局坐标

页面结构如下时,popup 的 z-index:200 和 tabbar 的 z-index:100 根本不在同一个比较维度:

page root
  ├── <view position:absolute>          ← 创建独立层叠上下文
  │     └── van-popup (z-index: 200)    ← 仅在父容器内排序,相当于"容器级 200"
  └── <x-tabbar position:fixed>         ← 在 page root 层级参与排序
        z-index: 100                    ← 与容器本身比,而非与 popup 比

浏览器最终比较的是 position:absolute 容器与 position:fixed tabbar 的 z-index,而不是 popup 与 tabbar 的 z-index。把 popup 的 z-index 改成多少都没用,因为它被困在容器的"局部宇宙"里。

正解

不要试图在错误的维度调 z-index。有三种方案,按推荐程度排序:

方案一(推荐):弹窗打开时隐藏 fixed 元素

最轻量、最可靠。给 tabbar 组件加一个 show prop,弹窗打开时传 false 让 tabbar 淡出,弹窗关闭后恢复。

tabbar 组件添加 show 属性:

// tabbar/index.js
properties: {
  show: { type: Boolean, value: true }
}
<!-- tabbar/index.wxml -->
<view class="tabbar-float {{show ? '' : 'tabbar-hidden'}}">
/* tabbar/index.wxss */
.tabbar-float {
  transition: opacity 0.3s ease; /* 与 van-popup 动画时长对齐 */
}
.tabbar-hidden {
  opacity: 0;
}
.tabbar-hidden .tabbar-inner {
  pointer-events: none; /* opacity:0 不阻止点击,必须显式禁用 */
}

页面层联动:

<x-tabbar show="{{!showFilterPopup && !showCityPopup}}" />
坑:opacity:0 不等于不可点击

微信小程序中 opacity: 0 的元素仍然拦截点击事件。不加 pointer-events: none,用户点弹窗内容时会命中透明的 tabbar,造成误触。注意 pointer-events: none 要加在 .tabbar-inner(实际覆盖区域)上,而不只是最外层容器。

方案二(备选):root-portal

微信基础库 2.25.2+ 支持 <root-portal>,可以将内容提升到页面根节点渲染,从而脱离容器的层叠上下文:

<root-portal>
  <van-popup show="{{showPopup}}" ...>
</root-portal>

van-popup 组件内部也有 root-portal 选项,但版本兼容性和组件内部实现有冲突风险,视项目基础库版本选用。

附:背景色断层问题

bottom: calc(tabbar高度) 给内容面板预留空间时,底部会露出一截背景色,与面板颜色不连续。

/* Before:底部 gap 暴露不同背景 */
.listPanel { bottom: calc(140rpx + env(safe-area-inset-bottom)); }
 
/* After:容器铺满,内容 padding 避让 */
page { --tabbar-occupy: calc(140rpx + env(safe-area-inset-bottom)); }
.listPanel { bottom: 0; }
.list-scroll { padding-bottom: calc(var(--tabbar-occupy) + 16rpx); }

容器铺满后 tabbar 毛玻璃效果下透出内容,背景无断层。

正解:在弹窗状态维度解决,而非 z-index 维度

跨层叠上下文的 z-index 比较是无效的。正确姿势是:弹窗打开时主动隐藏 fixed 元素(opacity 动画 + pointer-events: none),或用 root-portal 让弹窗脱离容器。选 opacity 而非 wx:if,避免组件销毁/重建带来的闪烁。

一句话外卖

z-index 比大小的前提是两个元素在同一个层叠上下文里;只要中间隔着一个 position 容器,数字再大也没用——要么让弹窗逃出容器,要么让竞争对手在弹窗期间退场。