引言
最近在开发中,样式都写好了,在安卓与桌面浏览器表现正常,但 iOS Safari/微信内置浏览器 上出现了弹窗、遮罩、下拉菜单"盖不住/被压住/抽风"的问题。表面看是 z-index 失效,实则多半是 叠层上下文(stacking context) 被意外创建,导致你处于不同叠层里"再高也盖不到"。
常见现象
position: fixed的弹窗/遮罩在某些页面元素之下。- Popover/Select/日期面板被滚动容器裁剪或卡在容器里。
- 页面滚动时,浮层抖动/错位(尤其 iOS 唤起键盘后)。
- 某些视频/地图/iframe 总是浮在最上层或最下层。
根因速览(都是"造叠层"的元凶)
下列任意一个 出现在目标元素的任意祖先 上,都会创建新的 stacking context,让子孙的 z-index 和外界"脱钩":
transform/perspective/transform-stylefilter/backdrop-filter/mix-blend-modeopacity < 1/isolation: isolate/will-changeposition: 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 (容器结构 + 关键样式),我可以帮你指出具体是哪一层在造叠层并给出最省改动的修法。