讲解

元素在屏幕上除了 x、y 两个维度,还有垂直于屏幕的 z 轴顺序——谁盖住谁。默认绘制顺序是:背景和边框 → 负 z-index 子元素 → 块级盒子 → 浮动 → 行内 → z-index: auto/0 的定位元素 → 正 z-index 定位元素。z-index 只对「定位元素(position 非 static)以及 Flex/Grid 容器的子项」生效,给普通静态元素设 z-index 是无效的。

真正难的是「层叠上下文」(stacking context):某些元素会形成一个独立的 z 轴小世界,内部所有层叠只在这个世界里排序,世界之间的比较只看「根」的层级。创建层叠上下文的常见条件:position 为 absolute/relative 且 z-index 非 auto;position: fixed/sticky;opacity < 1;transform、filter、perspective 非 none;isolation: isolate;z-index 非 auto 的 Flex/Grid 子项等。这解释了最迷惑的 bug:元素 A 设了 z-index: 9999 还是被元素 B 盖住——因为 A 的祖先创建的层叠上下文整体层级比 B 低,A 在自己世界里再高也没用。

工程对策有三条:一是保持 z-index 小规模有序,用 10、20、30 或语义化档位(dropdown: 100、sticky: 200、modal: 300)而不是随手 9999;二是弹层类元素尽量挂在 body 直接子级,避开业务组件无意创建的层叠上下文;三是调试时在 DevTools 的 Layers/3D 面板里直观看层叠上下文树,比猜快得多。负 z-index 可以把元素藏到父级背景之后,但父级一旦创建层叠上下文(比如有背景色的 stacking context),元素会显示在父级背景之上——行为符合规范但常出人意料。

示例

/* 语义化 z-index 档位:集中管理,拒绝 9999 军备竞赛 */
:root {
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-modal: 300;
  --z-toast: 400;
}
.dropdown {
  position: absolute;
  z-index: var(--z-dropdown);
}
.modal {
  position: fixed;
  z-index: var(--z-modal);
}

/* 层叠上下文陷阱演示:badge 的 9999 救不了它 */
.card-a {
  position: relative;
  z-index: 1; /* card-a 创建层叠上下文,整组层级为 1 */
}
.card-a .badge {
  position: absolute;
  z-index: 9999; /* 只在 card-a 内部有效 */
}
.card-b {
  position: relative;
  z-index: 2; /* 整组压过 card-a 及其内部一切 */
}

/* 主动创建干净的层叠上下文,隔离内部层叠 */
.widget {
  position: relative;
  isolation: isolate; /* 不依赖 z-index 也能创建层叠上下文 */
}

常见坑

  • 给 static 元素设 z-index:不生效。先给它 position: relative(不偏移)再谈层级。
  • z-index: 9999 军备竞赛:你 9999 我 99999,最后谁都盖不住谁。集中定义档位常量,全项目引用。
  • 忽视 transform/opacity 创建的层叠上下文:给容器加了个 transform 动画,里面的 fixed 弹层、dropdown 全被压住了——容器成了独立小世界。
  • 负 z-index 元素「消失」:它被画到了某个祖先的背景下面(通常因为祖先没有创建层叠上下文,元素落到了更远的背景后)。要么抬高它,要么给目标父级 isolation: isolate 让它只落在父级背景上。
  • 兄弟元素同为定位时纠结 z-index:z-index 相同(含都未设置)的定位元素,HTML 里后写的在上面——很多时候删掉多余的 z-index,调一下 DOM 顺序就够了。

小结

z-index 只对定位元素和 Flex/Grid 子项生效;层叠上下文是独立的 z 轴小世界,跨世界比较只看根的层级;用少量语义化档位管理 z-index。下一章处理另一个日常问题:内容溢出。