引言
最近在开发中,样式都写好了,在安卓与桌面浏览器表现正常,但 iOS Safari/微信内置浏览器 上出现了弹窗、遮罩、下拉菜单"盖不住/被压住/抽风"的问题。表面看是 z-index
失效,实则多半是 叠层上下文(stacking context) 被意外创建,导致你处于不同叠层里"再高也盖不到"。
常见现象
position: fixed
的弹窗/遮罩在某些页面元素之下。- Popover/Select/日期面板被滚动容器裁剪或卡在容器里。
- 页面滚动时,浮层抖动/错位(尤其 iOS 唤起键盘后)。
- 某些视频/地图/iframe 总是浮在最上层或最下层。
根因速览(都是"造叠层"的元凶)
下列任意一个 出现在目标元素的任意祖先 上,都会创建新的 stacking context,让子孙的 z-index
和外界"脱钩":
transform
/perspective
/transform-style
filter
/backdrop-filter
/mix-blend-mode
opacity < 1
/isolation: isolate
/will-change
position: sticky
(在吸附时)contain
/clip-path
/mask
- 滚动容器上的
-webkit-overflow-scrolling: touch
(iOS 特有,常与fixed
冲突) - 祖先设置
overflow: hidden/auto
导致裁剪(不是叠层,但会"看起来像盖不到")
结论:不是
z-index
的数字不够大 ,而是比较对象不在同一叠层。
快速定位(三步走)
1)把浮层临时挂到 <body>
看是否恢复
如果挂到 body
就好 ------ 说明原位置被祖先叠层限制了。
xml
<!-- Vue 3 -->
<teleport to="body">
<Modal v-if="open" />
</teleport>
<!-- Vue 2(portal-vue) -->
<portal to="body">
<Modal v-if="open" />
</portal>
2)审祖先链:逐级禁用"造叠层"的样式
在浏览器 DevTools 中沿父链检查并临时去掉:transform
、filter
、opacity
、isolation
、-webkit-overflow-scrolling: touch
、overflow
等,找到第一个导致问题的祖先。
小工具函数(页内快速排查):
javascript
function findStackingAncestor(el) {
const bad = ['transform','filter','opacity','mixBlendMode','perspective','isolation','willChange','backdropFilter','contain','clipPath','mask'];
let p = el.parentElement;
while (p) {
const cs = getComputedStyle(p);
if (
cs.transform !== 'none' ||
+cs.opacity < 1 ||
cs.filter !== 'none' ||
cs.perspective !== 'none' ||
cs.mixBlendMode !== 'normal' ||
cs.isolation === 'isolate' ||
cs.backdropFilter !== 'none' ||
cs.contain !== 'none' ||
cs.clipPath !== 'none' ||
cs.mask !== 'none' ||
cs.willChange !== 'auto'
) return p;
p = p.parentElement;
}
return null;
}
3)检查是否被裁剪
若你的浮层是 absolute
,而祖先 overflow: hidden/auto
,视觉上会被切断。要么挪层级,要么把 overflow
放到更"深"的包裹层上。
解决方案(按优先度)
方案 1:Portal 到 body + 固定定位(通用兜底)
css
/* 全局遮罩/弹窗的推荐样式 */
.modal {
position: fixed;
inset: 0; /* top/right/bottom/left:0 */
z-index: 2147483647; /* 顶层,避免随意被超越 */
}
-
弹层/下拉/气泡尽量 append 到 body (UI 库通常有
appendToBody
/getContainer
)。 -
打开弹层时禁用页面滚动:
css.is-modal-open { overflow: hidden; touch-action: none; }
csharpdocument.body.classList.add('is-modal-open'); // 打开 document.body.classList.remove('is-modal-open'); // 关闭
方案 2:清掉叠层制造者
- 把
transform
/filter
/opacity
等从祖先挪走,或只加到不影响浮层祖先链的元素上。 - iOS 专项:滚动容器 尽量别用
-webkit-overflow-scrolling: touch
,或者弹层打开时先移除,关闭再恢复(很多"fixed 失效/穿透"都与它有关)。
方案 3:给 sticky/吸顶元素明确层级
css
.sticky-header {
position: sticky;
top: 0;
z-index: 9999; /* 同叠层内排在上面 */
}
并确认它的父链没有 transform
等属性。
方案 4:避免被容器裁剪
- 不要让触发器与面板在同一"裁剪容器"里;让面板 portal 出去。
- 实在要在容器内展示,改用
position: sticky
或在容器内再建一层"浮层层"。
典型场景与套路解
场景 A:弹窗/遮罩盖不住视频/地图
- 遮罩 append 到 body ,
position: fixed
+ 高z-index
。 - 避免背景大容器上有
transform
。 - 个别 WebView 对
<video>
/iframe
合成层处理特殊,必要时给视频加position: relative; z-index: 0;
并移出叠层祖先。
场景 B:下拉菜单被列表滚动容器截断
- 容器通常有
overflow: auto
和-webkit-overflow-scrolling: touch
。
→ 将下拉菜单 portal 到 body 。
→ 或把overflow
调整到更深一层,只包内容不包触发器。
场景 C:iOS 键盘顶起后 fixed 底栏错位
-
底栏挂 body,
position: fixed; bottom: 0;
,并用安全区:css.toolbar { position: fixed; left: 0; right: 0; bottom: 0; padding-bottom: env(safe-area-inset-bottom); }
-
避免其祖先存在
transform
;必要时在键盘弹出时移除滚动容器的-webkit-overflow-scrolling: touch
。
最佳实践清单(落地就稳)
- 所有浮层/遮罩/菜单面板 → 默认 append 到 body
- 浮层样式统一:
position: fixed; inset: 0; z-index: 2147483647;
- 组件库统一开启
appendToBody/getContainer
- 页面上避免 在顶层容器使用:
transform / filter / opacity<1 / isolation / backdrop-filter / contain / clip-path / mask
- 滚动容器谨慎使用
-webkit-overflow-scrolling: touch
(iOS 专坑) - 有
sticky
的地方补上明确的z-index
,并保证其父链干净 - 任何需要"越界"的面板都不要放在有
overflow
的容器里
最小复现 & 对照修复
错误版(会被裁剪/盖不住)
xml
<div class="card-list has-transform">
<button id="btn">打开弹层</button>
<div class="modal">...</div> <!-- 放在有 transform 的祖先里 -->
</div>
css
.has-transform { transform: translateZ(0); } /* 造叠层 */
.modal { position: fixed; inset: 0; z-index: 9999; } /* 看似很高,但比错对象了 */
正确版(portal 到 body)
xml
<button id="btn">打开弹层</button>
<!-- Teleport / Portal 到 body -->
<div class="modal">...</div>
css
.modal { position: fixed; inset: 0; z-index: 2147483647; }
结语
iOS 上的 z-index
"乱套"并不是数字大小的问题,而是比较对象不在同一叠层 。实战中遵循"浮层挂 body + 清理叠层祖先 + 避免裁剪和滚动特性干扰 "的三板斧,基本都能搞定。如果你贴一个最小可复现的 DOM/CSS (容器结构 + 关键样式),我可以帮你指出具体是哪一层在造叠层并给出最省改动的修法。