换一个角度理解 层叠上下文、z-index

1,138 阅读6分钟

前言

在我们实际开发过程中,时常会遇到层级问题,但是当使用 z-index 时效果又不如预期,本质上还是对 层叠上下文 的了解不够深入导致,以下将从几个角度重新理解层叠上下文

层叠上下文

层叠上下文(stacking context),是HTML中一个三维的概念。简单的说层叠上下文类似于 Photoshop 中的 ,而元素则对应组中的图层以此来体现层叠关系。

04-1.jpg

层叠等级

  • 在层叠上下文中,表示的是层叠上下文元素在Z轴上的堆叠顺序
  • 在其他元素中,表示普通元素在Z轴上的堆叠顺序

那么这里就产生了一个疑惑,那么既然层叠等级指层叠顺序,那为什么z-index的设置会不如预期,如图.tooltipz-index 显然大于 header,但结果不如预期

04-2.jpg

为什么

因为层叠等级的比较需要在相同的层叠上下文中比较才有意义,不同层叠上下文中比较层叠等级是没有意义的,同理与 Photoshop 相同的组别内的图层堆叠等级比较才有意义,那么我们将上图的结构进行解析可以得到:

04-3.jpg

可以很明显的看出.tooltip 的 9999 仅仅是在 main 这个层叠上下文中进行比较

修复示例

我们仅仅需要将 .main 中的 z-index 去掉即可

04-4.jpg

去掉之后我们的层叠上下文如下所示

04-5.jpg

我们的tooltipheader处在了同一上下文中进行比较,那么为什么去掉 mainz-index 之后他的层叠上下文就消失了呢?这要涉及到层叠上下文的创建规则

创建层叠上下文

  1. HTML中的根元素<html></html>本身j就具有层叠上下文,称为“根层叠上下文”

  2. position 属性为 relativeabsolute并设置z-index属性

  3. position 属性为 fixedsticky (无需 z-index 直接产生上下文)

  4. opacity 小于 1

  5. mix-blend-mode 不为 normal

  6. display: flexdisplay: grid并给子元素设置z-index属性

  7. 使用transform, filter, clip-path, 或perspective

  8. will-change 值为 opacitytransform

  9. 显性地创建层叠上下文 isolation: isolate

    完整列表:MDN

    常见误解

04-6.jpg

第二个盒子显然通过z-index堆叠与其他兄弟节点之前,但是却没有设置 position 相关属性,这是由于在 Flexbox 规则中添加了一个例外: flex 子元素即使 positionstatic 依然可以使用 z-index

因此说所有元素都可以使用 z-index 是一个误区,Flexbox 是一个特例

稍等一下......

这里有一些奇怪的事情值得我们思考一两分钟

在 Photoshop 中,之间是有明显差异的,可视元素为,而则在结构上包裹,这是一种很清晰的概念;但在web中,这种差异就显得模糊了,任何想要使用z-index的元素都需要创建层叠上下文;

而当我们决定给元素添加一个z-index时,我们的目的又变成在 该元素的父级层叠上下文中降低或提升该元素的层叠等级。

当我们为此创建层叠上下文时,那么该元素就变成了一个组,原先该组的子元素层叠等级被扁平化包裹在组内,同时也将子元素牢牢锁在这个组中

我们重新看回示例代码:

<header>
  不你不能
</header>
<main>
  <div class="tooltip">
    我想站前面
  </div>
  <p>Some main content</p>
</main>

在默认情况下,HTML 元素会根据DOM的顺序进行堆叠,也就是说我们并不需要任何的 CSS 介入也可以让 main 现实在 header 之前,我们随后通过z-index改变了header的堆叠顺序,但于此同时header的子元素也全部被扁平化。

我们不能单纯的将z-index理解为改变层叠顺序的一种方式,同时也应该将z-index理解为将其子元素组成小组的一种方式

那或许你会说为什么不能讲z-index进行全局比较模糊组的概念,试想一下如果开发过程中每个元素都要全局比较那10000个元素又怎么比较?

同时也能利用这一特性来“封闭”元素,对于像 React 这种组件驱动的框架及其有用

isolation 抽象概念

这是一个非常有用的 CSS 属性,同时也是最令人感到疑惑的属性质疑,这或许是一个 “宝藏”属性

如何使用:

.wrapper {
  isolation: isolate;
}

当使用这个属性时,他只做一件事情:创建一个层叠上下文

那么已经有了上述这么多创建层叠上下文的方法,为什么我们又需要这一个呢?

反观其他创建方式,你会发现他们都是通过一些其他修改来隐式创建的,而 isolation 则是显性创建:

  • 不需要规定 z-index 的值
  • 可以作用到 static 元素
  • 不会影响到子元素

示例

有这样一个需求:当鼠标移入展开信封

04-7.gif

结构如下:

04-8.jpg

示例代码如下:

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

乍一看似乎没有问题,但当我们添加一个header并设置z-index:3,问题出现了,虽然四个组件包裹在一个div中但并没有创建层叠上下文,导致z-index开始于外部进行比较,这些层开始交织在一起

04-9.gif

我们通过使用 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 永远正确并且适用于其他页面吗?显然不是,这种隔离的概念巧妙地增加了组件的灵活性。

开发调试

目前在 Microsoft Edge 已经实现了 3D 视图功能,这个功能对层叠上下文调试非常有用

04-10.jpg

注意

Chrome 中的 layer 看起来很类似,但其实不然,图层跟层叠上下文不是一个概念,一个图层可以包含多个层叠上下文,这里的图层指的是 图形层,要讲图形层又涉及到 渲染层合成层,感兴趣可以看这里

插件

  1. 在 Chrome 中调试可以使用插件 CSS Stacking Context inspector

04-11.jpg

  1. 通过VS code 插件在开发过程中及时发现问题 :VSCode extension that highlights when stacking contexts are created

参考

  1. www.joshwcomeau.com/css/04/stac…