层叠上下文一图搞懂:z-index 失效之谜

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-17 13:16 ·5 浏览 ·0 回复

z-index 失效,绝大多数情况不是浏览器 bug,而是元素被"关"进了一个层叠上下文(stacking context,可理解为一个独立排序的三维区域):它只能和同一层叠上下文内的兄弟比高低,父级所在的那一层整体排在别人下面时,子元素写 z-index: 99999 也翻不出去。

一张图记住:同一层叠上下文内,从上到下的七层顺序

结论:同一个层叠上下文里,元素不是按 z-index 数值单独排序,而是先分层、再层内排序。从下到上依次是:

  1. 上下文的背景与边框(根元素或创建上下文的元素自身)
  2. 负 z-index 的子层叠上下文(z-index: -1 这类)
  3. 块级、非定位、普通文档流的后代(背景+边框+内容)
  4. 浮动元素(float,且未定位)
  5. 行内级、非定位、普通流后代(含 inline / inline-block 的文本内容)
  6. z-index: auto 或 0 的定位元素,以及靠其他属性创建的层叠上下文
  7. 正 z-index 的子层叠上下文

关键点在第 6 层:只要是一个层叠上下文,它连同它的全部子孙一起参与排序,整体挪动。这就是为什么给子元素狂加 z-index 没用——你动的是楼里的房间号,不是整栋楼在小区里的位置。

z-index 什么时候根本不生效

结论:z-index 只对定位元素(position 为 relative / absolute / fixed / sticky)和 flex、grid 项目生效;普通文档流里的 div 设 z-index 等于白写。

这是第一条要排查的。如果你的元素是 `position: static` 的普通 div,加多少 z-index 都不参与比较。补一句:`position: fixed` 和 `sticky` 本身就创建层叠上下文,不需要 z-index 触发。

谁在"偷偷"创建层叠上下文

结论:除了「定位 + z-index 非 auto」,还有一批属性会静默创建层叠上下文,这是 z-index 突然失效的头号元凶。

最常见的几个:

  • `opacity` 小于 1
  • `transform`、`filter`、`backdrop-filter`、`perspective`、`clip-path`、`mask` 非 none
  • `will-change: transform / opacity` 等
  • `isolation: isolate`
  • `contain: layout / paint / strict / content`
  • `mix-blend-mode` 非 normal
  • flex / grid 项目且 z-index 非 auto

工程里最典型的翻车场景是给卡片加了一句 `transform: translateZ(0)` 做"硬件加速",或者写了个 `opacity: .99` 的入场动画,结果里面的下拉菜单从此永远被隔壁卡片盖住。附带一个副作用:祖先有 transform / filter 时,后代的 `position: fixed` 不再相对视口定位,而是相对这个祖先——很多人把它也误当成"z-index 失效"。

三步定位:找出是谁把你关起来了

结论:先找到离你最近的那个"有罪祖先",问题就解决一半。DevTools 里 Chrome 的 Layers 面板可以直接看到页面上存在哪些层叠上下文;Firefox 的 3D 视图更直观。

不想翻面板就贴这段到控制台:

function stackingAncestors(el) {
  const out = [];
  for (let n = el.parentElement; n && n !== document.documentElement; n = n.parentElement) {
    const s = getComputedStyle(n), r = [];
    if (s.position !== 'static' && s.zIndex !== 'auto') r.push('positioned+z-index');
    if (+s.opacity < 1) r.push('opacity');
    if (s.transform !== 'none') r.push('transform');
    if (s.filter !== 'none') r.push('filter');
    if (s.isolation === 'isolate') r.push('isolation');
    if (s.willChange !== 'auto') r.push('will-change');
    if (s.contain !== 'none') r.push('contain');
    if (s.mixBlendMode !== 'normal') r.push('mix-blend-mode');
    if (r.length) out.push([n, r]);
  }
  return out;
}
stackingAncestors(document.querySelector('.my-dropdown'));

输出的每一行都是罪证。最近的那一个,就是限制你天花板的父级。

三种修法,按推荐度从高到低

结论:能改结构就别堆数字,堆数字只是在同一个笼子里打架。

  1. 统一上下文:给互相遮挡的两块内容所在的共同祖先加上 `isolation: isolate`,让它们进入同一个层叠上下文,然后只调这两块的 z-index。这是最干净的做法。
  2. 调父级而不是子级:确认两个冲突元素各自的父级层叠上下文,把父级的 z-index 拉开差距,子级内部随意。
  3. 搬家:下拉菜单、Tooltip、弹窗直接挂到 `body` 下(React 的 Portal、Vue 的 Teleport 都是干这个的),彻底脱离祖先上下文;位置用 JS 计算或 `position: fixed` 配合 getBoundingClientRect。

避坑提醒:改完别忘了检查祖先有没有残留的 `transform` 和 `opacity` 动画结束态为 0.99 这类写法,它们会持续创建上下文。

一句话收束:z-index 决定的是"在哪个笼子里排第几",层叠上下文决定的是"笼子本身排第几"。下次失效,先别加数字,先问一句——我的笼子是谁?

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-430.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
CLARA轻量论坛系统

全部回复 0

还没有回复,来抢沙发~