CSS 层叠与 z-index 机制
z-index 不是简单的数字比较
很多前端工程师以为 z-index 就是"数字越大越在上层",这是一个常见的误解。z-index 的实际工作机制远比这复杂:它只在同一个层叠上下文(Stacking Context)内才有意义。当一个元素创建了层叠上下文,它的子元素的 z-index 无法超越父元素在外部层叠上下文中的排序。
理解 z-index 不只是为了处理模态框的遮盖问题,更是为了理解 CSS 渲染引擎如何将二维的 HTML 文档映射到屏幕上的像素。
面试定位:z-index 是 CSS 面试的高频深度题。面试官通过层叠上下文的形成条件、z-index 的跨上下文隔离性、以及 negative z-index 的行为,问的是候选人对 CSS 渲染完整流程的理解。
层叠顺序(Stacking Order)
标准层叠顺序
在同一层叠上下文内,从后到前的顺序是:
1. 层叠上下文根元素的背景和边框
2. z-index < 0 的子元素(负 z-index)
3. 块级子元素(正常流中的块盒)
4. 浮动子元素(float)
5. 行内子元素(正常流中的行内盒)
6. z-index: auto / z-index: 0 的子元素
7. z-index > 0 的子元素(正 z-index)
实际例子
<div style="position: relative; z-index: 0;">
<!-- 这个 div 创建了层叠上下文 A -->
<div style="position: absolute; z-index: -1; bottom: 10px;">负z-index</div>
<!-- 会渲染在背景之下,父容器背景之上 -->
<div style="position: static;">块级元素</div>
<div style="float: left;">浮动元素</div>
<div style="display: inline;">行内元素</div>
<div style="position: relative; z-index: 0;">z:0</div>
<div style="position: absolute; z-index: 5;">正z-index</div>
</div>
层叠上下文的形成条件
主要形成条件
| 条件 | 说明 |
|---|---|
根元素 <html> |
页面最顶层的层叠上下文 |
position: fixed / sticky |
始终创建层叠上下文 |
position 非 static + z-index 非 auto |
定位元素创建 BFC,同时创建层叠上下文 |
z-index 非 auto 的 Flex 子项 |
Flex/Grid 容器内的子项 |
opacity < 1 |
即使是 0.99 也创建 |
transform 非 none |
即使是 scale(1) 也创建 |
filter 非 none |
即使是 blur(0) 也创建 |
mix-blend-mode 非 normal |
混合模式影响层叠 |
isolation: isolate |
强制创建新层叠上下文 |
will-change 指定了上述属性 |
提示浏览器提前优化 |
容易被忽略的陷阱
/* 这个元素创建了层叠上下文 */
.modal-backdrop {
position: fixed;
opacity: 0.5; /* opacity < 1 创建层叠上下文 */
z-index: 1000;
}
/* .modal-content 的 z-index 再高也受限于父元素的层叠上下文 */
.modal-content {
position: fixed;
z-index: 9999; /* 实际效果受限于父元素的 z-index: 1000 */
}
z-index 的比较规则
核心规则
- 只在同一层叠上下文内比较:不同层叠上下文的元素,z-index 没有可比性
- 祖先决定排序:子元素的层叠顺序不能超越父元素的层叠顺序
- z-index: auto 相当于 z-index: 0,但不创建层叠上下文(除非父元素是定位元素)
图解层叠上下文的隔离
层叠上下文 A (z-index: 2)
├── 子元素 A1 (z-index: 9999) ← 在上下文 A 内部排序
└── 子元素 A2 (z-index: 1)
层叠上下文 B (z-index: 1)
└── 子元素 B1 (z-index: 0)
结果:A1 即使是 9999,也在 A 整体之下(B 整体之上)
因为 B 的祖先层叠上下文 z-index 是 2 > 1
正确理解 z-index 的"越高越好"
/* 错误:试图用极高 z-index 覆盖另一个组件 */
.component-a {
position: relative;
z-index: 999999;
}
.component-b {
position: relative;
z-index: 1; /* 假设 .component-b 所在容器创建了 z-index: 1000000 的层叠上下文 */
}
/* 结果:.component-a 永远无法覆盖 .component-b,因为层叠上下文隔离 */
负 z-index 的行为
负 z-index 在层叠顺序中的位置
负 z-index 元素渲染在层叠上下文根元素的背景之后,但在正 z-index 元素之前:
.layer-example {
background: white; /* 背景层 */
}
.negative-z {
position: relative;
z-index: -1;
/* 这个元素渲染在 background 之后,但在其他内容之前 */
}
负 z-index 的常见用途
/* 1. 隐藏在内容后面 */
.behind {
position: absolute;
z-index: -1;
}
/* 2. 表单输入框的 focus 效果 */
.input-wrapper {
position: relative;
z-index: 0;
}
.input-wrapper::after {
content: "";
position: absolute;
inset: -2px;
border: 2px solid blue;
z-index: -1; /* focus 轮廓在输入框后面 */
}
/* 3. 调试定位问题 */
.debug-layer {
position: fixed;
z-index: -9999;
/* 将调试元素压在最底层 */
}
实战:解决 z-index 冲突
场景一:模态框的正确实现
.modal-overlay {
position: fixed;
inset: 0;
z-index: 1000; /* 确保在最上层 */
}
.modal-content {
position: relative;
z-index: 1001; /* 比 overlay 高 1,确保在其之上 */
}
场景二:下拉菜单覆盖页面内容
.nav-container {
position: relative;
z-index: 100; /* 导航容器的基础层级 */
}
.dropdown {
position: absolute;
z-index: 101; /* 比容器高,显示在下拉菜单上方 */
}
/* 弹窗覆盖下拉菜单 */
.modal {
position: fixed;
z-index: 1000; /* 覆盖所有页面内容 */
}
场景三:解决第三方组件的 z-index 问题
/* 第三方组件库使用了 z-index: 1000 */
.third-party-component {
position: relative;
z-index: 1000; /* 创建层叠上下文 */
}
/* 你的覆盖层 */
.my-overlay {
position: fixed;
z-index: 10000; /* 比第三方组件的层叠上下文祖先高 */
}
面试高频问题
Q: z-index 为什么有时候不生效?
回答要点:z-index 只在同一层叠上下文内比较。当父元素创建了层叠上下文(如 position: relative; z-index: 1),子元素的 z-index 再高也受限于父元素的排序。如果父元素的 z-index 是 1,子元素的 z-index 是 9999,也只能在父元素内部排序,无法超越另一个 z-index 为 2 的祖先层叠上下文。
Q: 哪些属性会创建层叠上下文?
回答要点:常见的包括:定位元素 + z-index 非 auto、opacity 小于 1、transform 非 none、filter 非 none、position: fixed/sticky 等。关键是这些属性的共同特征是改变元素的渲染上下文或影响其子孙的渲染顺序。
Q: z-index: auto 和 z-index: 0 有什么区别?
回答要点:两者在层叠顺序上效果相同(都相当于 0),但 z-index: auto 不会创建层叠上下文,而 z-index: 0 会创建。这意味着 z-index: 0 的子元素会受到父元素的层叠上下文限制,而 z-index: auto 的子元素不受此限制。