z-index 的作用
当元素重叠时,z-index 决定哪个绘制在前面。数字大的胜出。它对定位元素(position 不是 static 的元素)以及 flex 和 grid 项目有效:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>z-index</title>
<style>
body { font-family: system-ui, sans-serif; }
.stage { position: relative; height: 170px; }
.card {
position: absolute;
width: 130px; height: 100px; padding: 8px;
color: white; font-weight: 600; border-radius: 8px;
}
.a { background: #2563eb; top: 0; left: 0; z-index: 3; }
.b { background: #16a34a; top: 30px; left: 60px; z-index: 1; }
.c { background: #f59e0b; top: 60px; left: 120px; z-index: 2; }
</style>
</head>
<body>
<div class="stage">
<div class="card a" id="a">A z-index: 3</div>
<div class="card b" id="b">B z-index: 1</div>
<div class="card c" id="c">C z-index: 2</div>
</div>
</body>
</html>
没有 z-index 时,HTML 中靠后的元素会绘制在靠前的元素上面,所以 C 会盖住 B,B 会盖住 A。这里的 z-index 值把顺序反了过来:A 在最前面,然后是 C,再然后是 B。
取值
| 值 | 含义 |
|---|
auto(默认) | 和兄弟元素一起位于第 0 层,不创建层叠上下文 |
0 | 第 0 层,但会创建层叠上下文 |
| 正整数 | 位于较小值的前面 |
| 负整数 | 位于第 0 层后面;如果父元素形成了层叠上下文,还会位于父元素内容的后面 |
只有整数有效:z-index: 1.5 会被忽略。
为什么 z-index 9999 也会输:层叠上下文
z-index 的值并不是在整个页面范围内比较的。元素被分组到层叠上下文中,z-index 只给同一个上下文中的元素排序。然后这个上下文本身作为一个整体层,按创建它的元素的 z-index 放置。
下面,蓝色下拉菜单的 z-index: 9999,但它位于一个 z-index: 1 的头部里。绿色横幅是这个头部的兄弟元素,z-index: 2。整个头部层,包括下拉菜单在内,都在横幅下面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stacking context</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 14px; }
.header {
position: relative;
z-index: 1; /* creates a stacking context */
height: 40px; padding: 10px; background: #e5e7eb;
}
.dropdown {
position: absolute;
top: 40px; left: 20px;
z-index: 9999; /* only counts inside .header */
width: 180px; height: 110px; padding: 8px;
background: #2563eb; color: white;
}
.banner {
position: relative;
z-index: 2;
margin-top: 20px; height: 60px; padding: 10px;
background: #16a34a; color: white;
}
.no-context .header { z-index: auto; }
</style>
</head>
<body>
<div id="page">
<div class="header">Header, z-index: 1
<div class="dropdown" id="dropdown">Dropdown, z-index: 9999</div>
</div>
<div class="banner" id="banner">Banner, z-index: 2</div>
</div>
<p><button type="button" id="fix">Set the header's z-index to auto</button> <span id="out"></span></p>
<script>
const report = () => {
const r = document.getElementById('banner').getBoundingClientRect();
const top = document.elementFromPoint(r.left + 60, r.top + 20);
document.getElementById('out').textContent = 'On top at the overlap: ' + top.id;
};
document.getElementById('fix').addEventListener('click', () => {
document.getElementById('page').classList.toggle('no-context');
report();
});
report();
</script>
</body>
</html>
点击按钮。设为 z-index: auto 后,头部不再形成层叠上下文,所以下拉菜单的 9999 直接和横幅的 2 比较,下拉菜单就到了最前面。把下拉菜单提高到 99999 是永远不会有用的。
什么会创建层叠上下文
满足下面任何一条,元素就会成为自己的层叠上下文,把它内部所有元素的 z-index 都困在里面:
| 属性 | 条件 |
|---|
position: relative 或 absolute | z-index 不为 auto |
position: fixed 或 sticky | 始终 |
| flex 或 grid 项目 | z-index 不为 auto |
opacity | 小于 1 |
transform、scale、rotate、translate | 任何不为 none 的值 |
filter、backdrop-filter | 任何不为 none 的值 |
isolation | isolate |
mix-blend-mode | 任何不为 normal 的值 |
will-change | 指定了上面任一属性 |
contain | layout 或 paint(以及 strict、content) |
出人意料的是 opacity 和 transform:给父元素加一个淡入效果或悬停动画,可能会让它的下拉菜单突然跑到下一个区块的后面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>opacity creates a context</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 14px; }
.row { display: flex; gap: 0; }
.col { position: relative; width: 160px; height: 120px; padding: 8px; background: #e5e7eb; }
.col + .col { background: #fde68a; }
.tip {
position: absolute; top: 40px; left: 100px; z-index: 10;
width: 120px; padding: 8px; background: #2563eb; color: white;
}
.faded { opacity: 0.99; }
</style>
</head>
<body>
<div class="row">
<div class="col faded" id="first">opacity: 0.99
<div class="tip" id="tip">Tooltip z-index: 10</div>
</div>
<div class="col" id="second">Next column</div>
</div>
<p id="out"></p>
<script>
const r = document.getElementById('second').getBoundingClientRect();
document.getElementById('out').textContent =
'On top in the overlap: ' + document.elementFromPoint(r.left + 30, r.top + 60).id;
</script>
</body>
</html>
提示框的 z-index: 10 本应让它位于下一列之上。但 opacity: 0.99 让第一列成为 z-index 为 auto(第 0 层)的层叠上下文,而第二列在 HTML 中位于同一层的后面,所以它会画在整个第一列之上,提示框也包括在内。删掉 opacity 那一行,提示框就又回到前面了。
负 z-index
负 z-index 会把元素放到它的兄弟元素后面。如果父元素没有形成层叠上下文,它甚至会跑到父元素背景的后面,这常常让人意外。在父元素上设置 isolation: isolate 会创建一个上下文,而不带来任何其他视觉效果,从而把负值的子元素留在里面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Negative z-index</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 14px; display: flex; gap: 24px; }
.button {
position: relative;
padding: 14px 20px; background: #e5e7eb; font-weight: 600;
}
.button::before {
content: "";
position: absolute; inset: -6px;
background: #f59e0b;
z-index: -1; /* behind the button's text */
}
.isolated { isolation: isolate; }
</style>
</head>
<body>
<div class="button">No isolation</div>
<div class="button isolated">isolation: isolate</div>
</body>
</html>
没有 isolation 时,琥珀色边框掉到了按钮自身的灰色背景后面,只露出边缘。设置 isolation: isolate 后,边框留在按钮的层叠上下文里:在背景之上,在文字之下。
z-index 层级规范
为整个网站挑选几个命名的层级,而不是不断加大数字:
:root {
--z-dropdown: 10;
--z-sticky: 20;
--z-overlay: 30;
--z-modal: 40;
--z-toast: 50;
}
.modal { position: fixed; z-index: var(--z-modal); }
大多数层级问题都是层叠上下文的问题,而不是数字的问题,所以一套小的层级规范,再加上在内部使用 z-index 的组件上设置 isolation: isolate,就能解决大部分问题。z-index 需要定位才能生效;position 页面讲解了定位。
常见错误
- 给静态元素设置 z-index。加上
position: relative(或让它成为 flex 或 grid 项目)。
- 一路加到 9999。如果父元素形成的层叠上下文位于竞争对手下方,子元素的值再大也赢不了。应该去找出那个上下文。
- 父元素上新加的
opacity、transform 或 filter 突然把它的下拉菜单困住了。
- 使用小数。
z-index: 1.5 是无效的。
- 以为
z-index: 0 和 auto 表现相同。0 会创建层叠上下文;auto 不会。
- 模态框放在嵌套很深的组件里。把它们渲染在
<body> 的末尾附近(或使用 <dialog> 元素的顶层),这样就没有祖先上下文能困住它们。