z-index 调到最大也不管用?你撞上「层叠上下文」了

有天下午,我写一个下拉菜单,死活浮不起来。明明 z-index: 9999 都加上了,它还是乖乖躺在页面底下,像个社恐。

后来才知道,问题不在 z-index 的大小,而在「层叠上下文」。

层叠上下文是什么

想象一群人叠罗汉。你在一个罗汉堆里拼命把手举高(调 z-index),但另一个罗汉堆整体比你高,那你再怎么举也没用——你被你所在的那个堆「封顶」了。

在 CSS 里,每个元素最终是按「它属于哪个层叠上下文」来比高低的,不是全局比。同一个上下文内部比 z-index;不同上下文之间,比的是它们父级谁高。

哪些东西会「开一个新堆」

下面这些属性一旦出现在某个元素上,它自己就成了一个独立的层叠上下文:

  • position: fixed 或 sticky
  • position: relative/absolute 并且 z-index 不是 auto
  • opacity 小于 1(比如为了做淡入淡出设了 0.99)
  • transform、filter、perspective 不是 none
  • will-change 设了上面那些属性
  • isolation: isolate

我那个下拉菜单的锅,就是祖先上一个为了修 iOS 滚动卡顿加的 transform: translateZ(0) 干的——它默默开了一个新堆,把菜单的高度锁死在里面。

一段能直接粘进控制台的排查脚本

把那个「浮不起来」的元素丢进去,它会帮你列出所有可能开新堆的祖先:

function whyBehind(el) {
  const causes = [];
  let node = el.parentElement;
  while (node) {
    const s = getComputedStyle(node);
    if ((s.position === "relative" || s.position === "absolute" || s.position === "fixed") && s.zIndex !== "auto") {
      causes.push(`position:${s.position} + z-index:${s.zIndex}`);
    }
    if (s.opacity !== "1") causes.push(`opacity:${s.opacity}`);
    if (s.transform !== "none") causes.push(`transform:${s.transform}`);
    if (s.filter !== "none") causes.push(`filter:${s.filter}`);
    if (s.willChange && s.willChange !== "auto") causes.push(`will-change:${s.willChange}`);
    if (s.isolation === "isolate") causes.push("isolation:isolate");
    node = node.parentElement;
  }
  console.log("可能创建了层叠上下文的祖先:", causes);
  return causes;
}

// 用法:在元素上右键 → 检查 → 控制台里粘贴上面函数,然后:
// whyBehind($0)

$0 是开发者工具里当前选中的元素,特别好用。

怎么修

最快的办法:给「应该浮在最上面」的那个容器单独开一个干净的堆:

.dropdown {
  isolation: isolate; /* 在这个容器内部重新比高低,不受外面干扰 */
}

isolation: isolate 的意思是「我就想自己单开一摊,别让祖先的层级逻辑掺和进来」。比到处加 !important 和疯狂调 z-index 干净多了。

小结

z-index 不是越大越靠前,它只在同一个层叠上下文里比大小。遇到问题,先问一句:「我是不是被某个祖先关进另一个堆里了?」用上面的脚本一查,多半就现形了。