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 < 1、transform 等)的元素,会创建一个独立的层叠上下文。该上下文内的子元素 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 的元素仍然拦截点击事件。不加 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 比较是无效的。正确姿势是:弹窗打开时主动隐藏 fixed 元素(opacity 动画 + pointer-events: none),或用 root-portal 让弹窗脱离容器。选 opacity 而非 wx:if,避免组件销毁/重建带来的闪烁。
一句话外卖
z-index 比大小的前提是两个元素在同一个层叠上下文里;只要中间隔着一个 position 容器,数字再大也没用——要么让弹窗逃出容器,要么让竞争对手在弹窗期间退场。