【译】z-index 特喵到底是什么?

avatar
@智云健康

原文地址:www.joshwcomeau.com/css/stackin…

作者:Josh Comeau、 译者:林鸿鹄

未经授权禁止转载。

带你探索 CSS 层叠上下文, CSS 中头号误导人的机制。

在 CSS 中,我们都明确的知道用 z-index 可以来控制 HTML 的层级顺序。元素有着越大的指数将会排在页面的最顶部:

<style>
  .box {
    position: relative;
    width: 50px;
    height: 50px;
    border: 3px solid;
    background: silver;
  }
  .first.box {
    z-index: 2;
  }
  .second.box {
    z-index: 1;
    margin-top: -20px;
    margin-left: 20px;
  }
</style>

<div class="first box"></div>
<div class="second box"></div>

image.png

因为 .first.box 比 .second.box 有着更大的 z-index 的数值,所有它展示在前面。假如我们把 z-index 的声明从代码中移除的话,那么 .first.box 将会被 .second.box 遮挡了。

image.png

但事情往往不是这么简单。有时候大的 z-index 不是战无不胜的。让我们来看看小明同学写的这个代码到底发生了什么!

<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>

image.png

小明明明声明了.tooltip 的 z-index:999999, 是大于 header 的 z-index:2 的。 可是为什么 header 还是展示在最上层呢?

在揭秘这个神秘面纱之前,我们需要去学习一个叫做 stacking contexts 的知识点。这个只是点虽然让人费解,但却是CSS最为基础的机制。在剩下的这片文章中,我们将会了解到他们是什么?他们是怎样运作的,以及我们怎么能在代码中用他们取得便利。

本篇文章面向的读者:所有被 z-index 曾经支配过恐惧的前端开发。

层(layers)和组(groups)

如果你曾经用过类似 PS 或是 Figma 这种图文编辑软件,那么你应该熟悉层级的概念:

image.png

下面这张图像千层饼一样有3层不同的画布。最低层的是一张小喵咪的照片。在照片之上的是2层傻傻的细节,最后合成后就是一张长胡子的天使小猫咪了!

image.png

在PS里,我们可以组合这些层级:

image.png

就像文件夹一样,一个组可以让我们组合一系列的层级。组合之间的层级不能被混合在一起。所有狗狗的层级都会盖在猫咪的层级之上。

当我们导出最终组合的时候,我们一点也看不到猫咪,因为全部的层级都已经被猫咪覆盖了:

image.png

同样的,CSS层级的运作方式其实和 PS 差不多: 元素们都被组合成为 stacking contexts。当我们给元素一个z-index,那么这个值只会和在相同 context 下的其他元素竞争。z-index 不是全局的。

在默认的情况下, 一个简单的 HTML 文本只有一个上下文,它包括了所有的节点。 但是,我们可以添加更多的上下文!

创建上下文的方法有很多种, 但是最普遍的方法是通过结合两个声明,position 和 z-index :

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

当两个声明在一起的时候,一个秘密的开关被打开了: 我们就这样创建了一个新的上下文,元素 .some-element 和它的子元素将会被划分成一个组。

让我们来重新回顾一下之前的那个例子:

<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>

我们可以先画出这段代码的上下文结构:

image.png

虽然 .tooltip 元素的 z-index 是 999999,但是那个值只在 main 标签里才能生效。这个 z-index 只能控制 .tooltip 是展示在 p 标签的上方还是下方而已。

纵观根上下文,我们可以比较 header 标签和 main 标签所在的位置。 因为 main 标签的 z-index 比 header 小。所以 main 和它的子元素都展示在 header 的下方

修改上面的例子

所以我们该如何修改小明同学的代码来让 tooltip 展示在 header 的上方呢?其实非常的简单,我们完全不需要给 main 标签添加 z-index:

image.png

没了 z-index 的 main 标签不会去创建上下文。 现在我们再去看代码的上下文结构将会是这样的:

image.png

现在,因为 header 和 tooltip 元素在相同的上下文中,他们的 z-index 变开始较量,最后决出胜利者!

注意: 这里我们不是在讨论那些元素的父子关系。不管 tooltip 是多么复杂地被其他元素所嵌套,浏览器只关心层叠上下文。

打破规则

在上面更改代码的例子中,我们只是把 z-index 从 main 标签中移除了,因为刚刚 z-index 完全没有起什么作用。 但是如果 main 标签真的需要 z-index 来创建一个上下文呢?怎么样在不移除 z-index 的情况下来实现呢?

根据CSS的规则,很遗憾我们没有办法通过其他CSS方法达成我们要的效果:一个上下文里的元素永远都不能拿来和其他上下文比较。

幸运的是,我们还是可以通过其他方法来达成那样的效果,我们需要动一下小脑筋。

我们可以把 tooltip 移出 main 标签,挂在 body 标签下面,然后通过 CSS 定位来让 tooltip 看起来是 header 的子元素。

image.png

更多创建层叠上下文的方法

我们已经见到了如何通过结合 relative absolute 和 z-index 的方法来创建上下文,但是那不是唯一的方法!我们还可以通过这些法子:

  • 把透明度 opacity 设置成比 1 小的值
  • 把 position 设置为 fixed 或者 sticky (这种情况无需提供 z-index)
  • 把 mix-blend-mode 设置为 multiply、hard-light、difference(normal不行🙅)
  • 把 z-index 添加到一个带有 display:flex 或者 display: grid 的容器里
  • 使用 transform, filter, clip-path,或者 perpective
  • 把 will-change 设置为 opacity 或者 transform
  • 通过 isolation: isolation 直接创建上下文(很快就会讲到这个的使用!)
  • 其他详见 MDN 实现清单

下面这个例子中我们使用了 will-change 给 main 标签创建了一个上下文,所以我们依旧得到了之前的结果:

image.png

z-index 的常见误知

我们已经知道了为了让 z-index 生效,我们需要去给 position 设置成 relative 和 absolute 对吧?

其实不完全是这样的,再来看看下面这段代码:

image.png

第二个盒子被赋值了 z-index 被展示在其他盒子的上方。但是我们看不到哪里声明了 position 对吧!

大部分情况下,z-index 只能在拥有 position 的元素生效(relative/absolute)。但是在 flexbox 布局时,flex 子元素的 z-index 即使 position 为 static 也可以生效哦~

再让我们捋一捋。。

有件奇怪的事我们可能需要再仔细琢磨一下。

在之前的PS比喻中,我们能很好的区分组的概念和层级的概念:所有可视的元素都是层级,组只是一个来帮助组合层级的容器而已。

在浏览器里,这个概念就有一些些模糊。所有使用了 z-index 的元素创建了上下文。

一般来说当我们决定使用 z-index 的时候,我们的目的仅仅是希望改变那个元素在当前父上下文的位置。我们并不希望在那个元素上创建一个上下文!我们需要在这个上面再好好考虑一下。

当一个上下文被创建了,它会把所有子元素给扁平化。所有子元素给安排的明明白白,我们本质上把他们都锁在了内部。

我们不应该把 z-index 只想象成改变元素顺序的工具,我们也应该把它当成包裹(组合)它子元素的方法。如果组没有生成的话 z-index 是不会生效的。

我们已经见到了之前的例子,上下文有时候会造成细微的,难debug的情况。假如 z-index 能在全局进行比较会不会好一些呢?

我不觉得,这是一些我的理由:

  1. 假如我们有一个很复杂的结构,我们需要给很多很多元素赋予 z-index,z-index 通货膨胀了解一下!

  2. 虽然我不是个浏览器工程师,我能想象现在的设计有助于浏览器的执行效率。没有现在的设计,浏览器需要和许多有 z-index 的元素进行比较,感觉上会有很多额外的工作要做!

  3. 当我们理解了上下文,我们可以借此去封印一些元素。在组件驱动的框架里这是个非常强大的模式,比如 React。

最后一点是最有趣的点了,让我们多看看吧!

用 isolation 实现全密封抽象

现在我要和你们介绍我最喜欢的,也是最复杂的一个 CSS 属性: isolation 属性,隐藏在这个语言里的宝藏男孩。

你可以这样去使用它:

.wrapper {
  isolation: isolate;
}

当我们使用这个来声明一个元素,它制作了一件事情:创建一个新的上下文。

为什么有这么多方法来创建一个上下文呢?哈哈,因为所有其他的方法都是隐式创建了上下文,其他的更改可能会改变其结果。 但是 isolation 可以用最纯粹简单方法创造上下文。

  • 不需要去规定 z-index
  • 可以在 static 定位的元素上使用!
  • 不会影响到子元素的渲染

再强调一次,因为这个实在是太棒了!isolation 可以在 static 定位的元素上使用!它可以让我们封印它的子元素!

让我们再来看另外一个例子。最近我搭建了一个很赞的信封组件。

image.png

当你把鼠标移到开口处,一封信会像这样划出来

image.png

这个信封组件的结构分为四个部分(从左到右:信封后面的开口贴,信封后面,一封信,信的正面)

image.png

我把这个结构用 React 组件打包在一起,看起来是这个样子(为了让大家简单的看到,我使用了内敛样式)

function Envelope({ children }) {
  return (
    <div>
      <BackPane style={{ zIndex: 1 }} /> 信的正面
      <Letter style={{ zIndex: 3 }}> 信
        {children}
      </Letter>
      <Shell style={{ zIndex: 4 }} /> 信封后面
      <Flap style={{ zIndex: isOpen ? 2 : 5 }} /> 开口贴
    </div>
  )
}

(你可能好奇为什么开口贴的 z-index 是动态的, 因为它在打开时需要展示在信的后面)

image.png

一个好的 React 组件是到哪里都可以使用,是一个独立的封闭的,就像一件太空服。目前我们这个太空服很不幸漏气了。

image.png

image.png

当我把这个组件放在一个 z-index: 3 的 header 旁边时,如上图所示我们的层级和 header 被混淆在了一起。 因为我们的信封 Envelope 是由 div 包裹着四个层级,但它本身没有创建一个上下文。

但我们可以给 Envelope 组件外层的 div 添加 isolation: isolate 来保证组件能划成一个组。

function Envelope({ children }) {
  return (
    <div style={{ isolation: 'isolate' }}>
      <BackPane style={{ zIndex: 1 }} />
      <Letter style={{ zIndex: 3 }}>
        {children}
      </Letter>
      <Shell style={{ zIndex: 4 }} />
      <Flap style={{ zIndex: isOpen ? 2 : 5 }} />
    </div>
  )
}

那么,为什么我们不用之前的方法 position: relative; z-index:1 来创建上下文呢?那是因为 React 组件是期望被重复使用的, z-index:1 不能保证在其他情况下是正确的。isolation 的魅力就是你可以保证这个组件永远是灵活的。

浏览器支持 isolation 不是一个新的熟悉,他有很好的浏览器支持。它可以在除了 Internet Explorer 以外的任何浏览器上使用。

假如你想在 Internet Explorer 上支持,你可以考虑使用 transform:translate(0px)

Debug 上下文

很遗憾, 我没有找到很多能帮助 debug 上下文的工具。

Microsoft Edge 有一个很有趣的 "3D视图" 可以帮助显示上下文的结构。

image.png

实际上讲,这个东西其实我感觉看起来很累,不能感觉他能帮助我去定位我 app 中的元素,也不能帮我去理解我 app 里的上下文关系。

其实我还有一个另外的小点子,那就是 offsetParent。

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

offsetParent 会返回一个最近且带有非static值(relative, absolute, fixed)的position的祖先节点。

注意:这不是一个最完美的解决方法。不是所有上下文都是用 position 来布局的,而且不是所有 position 定位的元素都会创建一个上下文!但这个方法至少是一个用来检测的起点。

假如你有新的方法,可以通过 Twitter 来联系我!: twitter.com/JoshWComeau

更新1: Felix Becker 告诉我一个 VSCode 插件可以用来高亮生成上下文的代码:

image.png

(这个插件能在 .css 和 .scss 文件使用)

更新2: Giuseppe Gurgone 告诉我一个 谷歌浏览器插件可以在 devtools 添加新的 “z-index"

更新3: Andrea Dragotta 发明了一个超赞的浏览器插件,它可以让我们看到一大堆超有用关于 z-index 和上下文的信息:

image.png

这个东西真的是超赞超牛,我最近一直在用这个插件。

谷歌 火狐