什么是 z-index?
在 CSS
中,我们有一个工具可以显式地控制HTML
元素的堆叠顺序,那就是 z-index
。具有更高 z-index
值的元素会显示在上方:
由于 .first.box
的 z-index
值比 .second.box
大,所以它会堆叠在前面。如果我们去掉这个 z-index
声明,它就会落到后面。上面的代码是可编辑的——试试看吧!
然而,事情并不总是这么简单。有时候,更大的 z-index
值并不会获胜。
来看这里发生了什么:
1 | <style> |
1 | body { |
.tooltip
的 z-index
值远大于 header
!那为什么 header
会显示在上面呢?
要解开这个谜团,我们需要了解堆叠上下文,这是一种晦涩但基本的 CSS 机制。在本文中,我们将探讨它们是什么,它们如何工作,以及如何利用它们。
层和组
如果你曾经使用过 Photoshop
或 Figma
这样的图像编辑软件,你可能会熟悉图层的概念:
我们的图像有 3 个独立的画布,像煎饼一样堆叠。最底层是一张猫的照片,上面有两层添加了搞笑的细节。通过合并这些图层,我们得到一个最终的合成图:
在这些程序中,我们还可以对图层进行分组:
就像文件夹中的文件一样,组允许我们分段图层。在堆叠顺序方面,图层不允许在组之间“混合”:所有狗的图层将出现在所有猫的图层之上。
当我们导出合成图时,我们根本看不到猫,因为它被狗挡住了:
对于 CSS
来说,情况类似:元素被分组到堆叠上下文中。当我们为一个元素设置 z-index
时,该值只与同一上下文中的其他元素进行比较。z-index
值不是全局的。
默认情况下,一个普通的 HTML 文档会有一个包含所有节点的单一堆叠上下文。但我们可以创建额外的上下文!
有许多方法可以创建堆叠上下文,但以下是最常见的方法:
1 | .some-element { |
通过结合这两个声明,触发了一个秘密机制:创建了一个堆叠上下文,在该元素及其所有子元素周围形成了一个组。
让我们再看看上面的问题:
1 | <style> |
我们可以绘制出这个代码片段中创建的堆叠上下文:
- 根上下文
<header>
<main>
<div class="tooltip">
我们的 .tooltip
元素有一个 z-index
值为 999999,但该值仅在 <main>
堆叠上下文中相关。它控制的是工具提示是否显示在相邻的 <p>
标签上方或下方,仅此而已。
与此同时,在父上下文中,<header>
和 <main>
被比较。由于 <main>
的 z-index
值较小,它显示在 <header>
的下方。它的所有子元素都会随之移动。
解决我们的示例
我们如何解决工具提示问题?在这种情况下,我们实际上并不需要在 <main>
上创建堆叠上下文:
1 | <style> |
没有 z-index
,<main>
不会创建堆叠上下文。因此,我们的层次结构看起来像这样:
- 根上下文
<header>
<div class="tooltip">
由于 header
和我们的工具提示现在在同一个上下文中,它们的 z-index
值相互比较,工具提示胜出。
需要注意的重要区别是:我们不是在谈论父子关系。工具提示嵌套得比标题深并不重要。浏览器只关心堆叠上下文。
打破规则
在这个例子中,我们可以去掉<main>
的z-index
,因为它实际上并没有起作用。但是,如果我们确实需要<main>
使用z-index
或创建堆叠上下文呢?
根据 CSS 的规则,我们无法“突破”堆叠上下文。一个堆叠上下文内的元素永远不能与另一个堆叠上下文内的元素进行比较。
然而,我们仍然可以通过一些创造性的思维来实现所需的结果。
创建堆叠上下文
我们已经看到如何通过结合相对或绝对定位和z-index来创建堆叠上下文,但这并不是唯一的方法!还有其他方法:
- 将
opacity
设置为小于1的值 - 将
position
设置为fixed
或sticky
(这些值不需要z-index!
) - 应用非
normal
的mix-blend-mode
- 在
display: flex
或display: grid
容器内的子元素上添加z-index
- 使用
transform、filter、clip-path
或perspective
- 使用带有
opacity或
transform值的
will-change` - 显式地用
isolation: isolate
创建上下文(稍后会详细介绍!)
还有其他一些方法。你可以在MDN上找到完整列表。
这可能会导致一些意想不到的情况。看看这里发生了什么:
1 | <style> |
main不再设置z-index,但它使用了will-change,这是一个可以单独创建堆叠上下文的属性。
关于z-index的常见误解
为了让z-index
起作用,我们需要将position
设置为relative
或absolute
,对吗?
第二个盒子使用z-index
提升到了它的兄弟元素之上。代码片段中没有任何position
声明!
一般来说,z-index
只对“定位”元素(position
设置为非默认“static”的元素)起作用。但Flexbox
规范增加了一个例外:即使是静态定位的flex子元素也可以使用z-index
。
当我们决定给一个元素设置z-index
时,我们的目标通常是将该元素提升或降低到父堆叠上下文中的其他元素之上或之下。我们并不打算在该元素上创建堆叠上下文!但我们必须考虑这一点。
当一个堆叠上下文被创建时,它会“扁平化”它的所有子元素。这些子元素仍然可以在内部重新排列,但我们实际上已经将这些子元素锁定了。
1 | <header> |
默认情况下,HTML
元素将按照其DOM顺序进行堆叠。没有任何CSS
干扰,main
会渲染在header
之上。
我们可以通过给header
设置z-index
将其提升到前面,但不能在不扁平化所有子元素的情况下实现。这一机制导致了我们之前讨论的错误。
我们不应该仅仅将z-index
视为改变元素顺序的一种方式。我们还应该将其视为在该元素的子元素周围形成一个组的方式。z-index
只有在形成组的情况下才会起作用。
正如我们在工具提示演示中看到的,堆叠上下文可能会导致微妙的、难以诊断的错误。如果z-index
调试z-index
Microsoft Edge
有一个有趣的“3D视图”,可以让我们查看堆叠上下文:
在这个视图中定位一个特定的元素很难,而且我并不觉得它有助于我理解应用程序中的堆叠上下文。
你可以使用另一个有时有用的小技巧:offsetParent
。
1 | const element = document.querySelector('.tooltip'); |
offsetParent
返回最近的使用了static
以外position
值的祖先。它沿着树向上爬,寻找relative/absolute/fixed/sticky
祖先。
这不是一个完美的解决方案。并不是所有的堆叠上下文都使用定位布局,并不是所有定位元素都创建堆叠上下文!尽管如此,在实践中,这两者之间往往有很强的相关性。至少,这是一个起点。