理解z-index

什么是 z-index?

CSS 中,我们有一个工具可以显式地控制HTML元素的堆叠顺序,那就是 z-index。具有更高 z-index 值的元素会显示在上方:


由于 .first.boxz-index 值比 .second.box 大,所以它会堆叠在前面。如果我们去掉这个 z-index 声明,它就会落到后面。上面的代码是可编辑的——试试看吧!

然而,事情并不总是这么简单。有时候,更大的 z-index 值并不会获胜。

来看这里发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<style>
header {
position: relative;
z-index: 2;
}
.tooltip {
position: absolute;
z-index: 999999;
}
main {
position: relative;
z-index: 1;
}
</style>

<header>
My Cool Site
</header>
<main>
<div class="tooltip">
A tooltip
</div>
<p>Some main content</p>
</main>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
body {
background: #eee;
}

header {
height: 60px;
line-height: 60px;
background: pink;
text-align: center;
}

main {
padding: 32px;
}

.tooltip {
top: -12px;
left: 0px;
right: 0px;
margin: 0 auto;
width: 90px;
text-align: center;
padding: 8px;
background: white;
box-shadow: 1px 2px 8px hsla(0deg, 0%, 0%, 0.25);
border-radius: 6px;
}

.tooltipz-index 值远大于 header!那为什么 header 会显示在上面呢?

要解开这个谜团,我们需要了解堆叠上下文,这是一种晦涩但基本的 CSS 机制。在本文中,我们将探讨它们是什么,它们如何工作,以及如何利用它们。

层和组

如果你曾经使用过 PhotoshopFigma 这样的图像编辑软件,你可能会熟悉图层的概念:

我们的图像有 3 个独立的画布,像煎饼一样堆叠。最底层是一张猫的照片,上面有两层添加了搞笑的细节。通过合并这些图层,我们得到一个最终的合成图:

在这些程序中,我们还可以对图层进行分组:

就像文件夹中的文件一样,组允许我们分段图层。在堆叠顺序方面,图层不允许在组之间“混合”:所有狗的图层将出现在所有猫的图层之上。

当我们导出合成图时,我们根本看不到猫,因为它被狗挡住了:

对于 CSS 来说,情况类似:元素被分组到堆叠上下文中。当我们为一个元素设置 z-index 时,该值只与同一上下文中的其他元素进行比较。z-index 值不是全局的。

默认情况下,一个普通的 HTML 文档会有一个包含所有节点的单一堆叠上下文。但我们可以创建额外的上下文!

有许多方法可以创建堆叠上下文,但以下是最常见的方法:

1
2
3
4
.some-element {
position: relative;
z-index: 1;
}

通过结合这两个声明,触发了一个秘密机制:创建了一个堆叠上下文,在该元素及其所有子元素周围形成了一个组。

让我们再看看上面的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<style>
header {
position: relative;
z-index: 2;
}
.tooltip {
position: absolute;
z-index: 999999;
}
main {
position: relative;
z-index: 1;
}
</style>

<header>
My Cool Site
</header>
<main>
<div class="tooltip">
A tooltip
</div>
<p>Some main content</p>
</main>

我们可以绘制出这个代码片段中创建的堆叠上下文:

  • 根上下文
    • <header>
    • <main>
      • <div class="tooltip">

我们的 .tooltip 元素有一个 z-index 值为 999999,但该值仅在 <main> 堆叠上下文中相关。它控制的是工具提示是否显示在相邻的 <p> 标签上方或下方,仅此而已。

与此同时,在父上下文中,<header><main> 被比较。由于 <main>z-index 值较小,它显示在 <header> 的下方。它的所有子元素都会随之移动。

解决我们的示例

我们如何解决工具提示问题?在这种情况下,我们实际上并不需要在 <main> 上创建堆叠上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<style>
header {
position: relative;
z-index: 2;
}
.tooltip {
position: absolute;
z-index: 999999;
}
main {
position: relative;
/* 不再需要 z-index! */
}
</style>

<header>
My Cool Site
</header>
<main>
<div class="tooltip">
A tooltip
</div>
<p>Some main content</p>
</main>

没有 z-index<main> 不会创建堆叠上下文。因此,我们的层次结构看起来像这样:

  • 根上下文
    • <header>
    • <div class="tooltip">

由于 header 和我们的工具提示现在在同一个上下文中,它们的 z-index 值相互比较,工具提示胜出。

需要注意的重要区别是:我们不是在谈论父子关系。工具提示嵌套得比标题深并不重要。浏览器只关心堆叠上下文。

打破规则
在这个例子中,我们可以去掉 <main>z-index,因为它实际上并没有起作用。但是,如果我们确实需要 <main> 使用 z-index 或创建堆叠上下文呢?
根据 CSS 的规则,我们无法“突破”堆叠上下文。一个堆叠上下文内的元素永远不能与另一个堆叠上下文内的元素进行比较。
然而,我们仍然可以通过一些创造性的思维来实现所需的结果。

创建堆叠上下文

我们已经看到如何通过结合相对或绝对定位和z-index来创建堆叠上下文,但这并不是唯一的方法!还有其他方法:

  • opacity设置为小于1的值
  • position设置为fixedsticky(这些值不需要z-index!
  • 应用非normalmix-blend-mode
  • display: flexdisplay: grid容器内的子元素上添加z-index
  • 使用transform、filter、clip-pathperspective
  • 使用带有opacity或transform值的will-change`
  • 显式地用isolation: isolate创建上下文(稍后会详细介绍!)

还有其他一些方法。你可以在MDN上找到完整列表。

这可能会导致一些意想不到的情况。看看这里发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<style>
header {
position: relative;
z-index: 2;
}
.tooltip {
position: absolute;
z-index: 999999;
}
main {
position: relative;
/*
不再设置z-index…
但它还是出问题了??
*/
will-change: transform;
}
</style>

<header>
My Cool Site
</header>
<main>
<div class="tooltip">
A tooltip
</div>
<p>Some main content</p>
</main>

main不再设置z-index,但它使用了will-change,这是一个可以单独创建堆叠上下文的属性。

关于z-index的常见误解

为了让z-index起作用,我们需要将position设置为relativeabsolute,对吗?

第二个盒子使用z-index提升到了它的兄弟元素之上。代码片段中没有任何position声明!

一般来说,z-index只对“定位”元素(position设置为非默认“static”的元素)起作用。但Flexbox规范增加了一个例外:即使是静态定位的flex子元素也可以使用z-index

当我们决定给一个元素设置z-index时,我们的目标通常是将该元素提升或降低到父堆叠上下文中的其他元素之上或之下。我们并不打算在该元素上创建堆叠上下文!但我们必须考虑这一点。

当一个堆叠上下文被创建时,它会“扁平化”它的所有子元素。这些子元素仍然可以在内部重新排列,但我们实际上已经将这些子元素锁定了。

1
2
3
4
5
6
7
8
9
<header>
My Cool Site
</header>
<main>
<div class="tooltip">
A tooltip
</div>
<p>Some main content</p>
</main>

默认情况下,HTML元素将按照其DOM顺序进行堆叠。没有任何CSS干扰,main会渲染在header之上。

我们可以通过给header设置z-index将其提升到前面,但不能在不扁平化所有子元素的情况下实现。这一机制导致了我们之前讨论的错误。

我们不应该仅仅将z-index视为改变元素顺序的一种方式。我们还应该将其视为在该元素的子元素周围形成一个组的方式。z-index只有在形成组的情况下才会起作用。

正如我们在工具提示演示中看到的,堆叠上下文可能会导致微妙的、难以诊断的错误。如果z-index

调试z-index

Microsoft Edge有一个有趣的“3D视图”,可以让我们查看堆叠上下文:

在这个视图中定位一个特定的元素很难,而且我并不觉得它有助于我理解应用程序中的堆叠上下文。

你可以使用另一个有时有用的小技巧:offsetParent

1
2
const element = document.querySelector('.tooltip');
console.log(element.offsetParent); // <main>

offsetParent返回最近的使用了static以外position值的祖先。它沿着树向上爬,寻找relative/absolute/fixed/sticky祖先。

这不是一个完美的解决方案。并不是所有的堆叠上下文都使用定位布局,并不是所有定位元素都创建堆叠上下文!尽管如此,在实践中,这两者之间往往有很强的相关性。至少,这是一个起点。