css层叠上下文和z-index的使用和思考

过去一段时间经常遇到线上的页面元素互相遮盖的问题,索性就总结一下吧。

正常情况下,页面元素是从左到右和从上到下渲染(x、y 维度),但因为 margin 可以写负值,还有一些定位相关的 css 属性(absolute、relative、fixed、stick),这就会导致元素之间可能重叠,重叠后就需要判断元素堆叠顺序,这就涉及到层叠上下文(Stacking context)了,相当于增加了 z 轴的维度。

z-index

无新增层叠上下文的情况

我们先抛开层叠上下文的概念,看一下没有 z-index 或者其他特殊 css 属性正常情况下元素的堆叠规则。

按照元素出现的顺序依次堆叠下边的元素:

  1. 非定位的 block 元素,一般就是背景
  2. float 元素
  3. 非定位的 inline 元素,一般就是文字内容
  4. 定位元素,即 position 设置了 relative 或者 absolute

一句话总结就是同类型的后出现的覆盖先出现的,定位元素覆盖非定位元素。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.relative {
height: 50px;
position: relative;
top: 10px;
background: rgba(0, 0, 255);
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6);
}
.static1 {
background: rgba(255, 0, 0, 0.5);
height: 90px;
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6);
overflow: hidden;
width: 60vw;
}
.static2 {
color: red;
background: rgba(0, 255, 0);
height: 90px;
margin-top: -20px;
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6);
overflow: hidden;
width: 80vw;
}
.float {
background: rgba(255, 255, 0);
height: 100px;
margin-top: -20px;
box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.6);
float: left;
}
</style>
</head>
<body>
<div class="relative">我是 relative 元素</div>
<div class="static1">
我是 static1 元素<br />我是 static1 元素<br />我是 static1 元素<br />我是
static1 元素<br />我是 static1 元素<br />我是 static1 元素<br />我是
static1 元素<br />我是 static1 元素
</div>
<div class="static2">
我是 static2 元素,margin-top 是负值<br />我是 static2 元素,margin-top
是负值<br />我是 static2 元素,margin-top 是负值<br />我是 static2
元素,margin-top 是负值<br />我是 static2 元素,margin-top 是负值<br />我是
static2 元素,margin-top 是负值
</div>
<div class="float">我是float 元素</div>
</body>
</html>

image-20230622121535164

static 的背景看成 block 元素,文字看成 inline 元素。先堆叠 block 元素,再堆叠 float 元素,再堆叠 inline 元素,最后堆叠定位元素。

static2 的背景遮盖了 static1 的背景,但没有遮盖住 static1 的文字。

float 元素遮盖了 static2 的背景。

static2 的文字遮挡了 static1 的文字,因为 float 元素在 inline 元素之前进行了堆叠,所以 static2 的文字也遮盖了 float 的文字。

relative 元素最后堆叠,直接遮盖了 static1 的背景和文字。

含有新增的层叠上下文 Stacking context

考虑一下有新增的层叠上下文的情况。

概念

层叠上下文可以理解成一张画布,可以在上边独立地一层一层的刷染料。不同的层叠上下文就是不同的画布,他们之间互相独立。而且层叠上下文中也可以在再形成新的层叠上下文。

Diagram showing stacked rectangles conveying the three-dimensional, nested nature of stacking contexts

如何生成新的层叠上下文

总结下常用的:

html 元素。

position 为 absolute 或者 relative,并且 z-index 不是 auto。

position 为 fixed,无需设置 z-index 的值

flex 的子项,并且 z-index 不是 auto。

opacity 设置为小于 1。

上边的这些情况都会生成一个层叠上下文,在自己的层叠上下文内进行一层一层的渲染。

堆叠原则

同一个层叠上下文内元素的堆叠就是之前讨论的无新增层叠上下文的情况(之前的情况其实就是只有一个层叠上下文,即 html 元素自己生成了一个层叠上下文)。

同一层叠上下文中,层叠上下文之间堆叠顺序如下:

  1. 通过 z-index 加上某些条件生成的层叠上下文,并且 z-index 为负值
  2. 没有生成层叠上下文的元素,即之前讨论的无新增层叠上下文的情况
    1. 非定位的 block 元素,一般就是背景
    2. float 元素
    3. 非定位的 inline 元素,一般就是文字内容
    4. 定位元素,即 position 设置了 relative 或者 absolute,但没设置 z-index
  3. 通过 z-index 加上某些条件生成的层叠上下文,并且 z-index 为 0 或者其他条件生成的层叠上下文
  4. 通过 z-index 加上某些条件生成的层叠上下文,并且 z-index 为正值,值越大越在上边。

一个层叠上下文中可以一直嵌套的生成新的层叠上下文,如果要比较不同的层叠上下文下元素的层级关系,首先需要找到当前元素所在的层叠上下文(它所在的层叠上下文又在另一个层叠上下文之中,一直向上找,直到找到从它们共同层叠上下(比如 html 元素)中生成的那个层叠上下文),接着按照堆叠规则比较它们所在的层叠上下文关系即可。

看一个经典的例子

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.red,
.green,
.blue {
position: absolute;
width: 100px;
color: white;
line-height: 100px;
text-align: center;
}

.red {
z-index: 1;
top: 20px;
left: 20px;
background: red;
}

.green {
top: 60px;
left: 60px;
background: green;
}

.blue {
top: 100px;
left: 100px;
background: blue;
}
</style>
</head>
<body>
<div>
<span class="red">Red</span>
</div>
<div>
<span class="green">Green</span>
</div>
<div>
<span class="blue">Blue</span>
</div>
</body>
</html>

首先观察除了 html 元素有没有新的层叠上下文。

有一个新生成的层叠上下文:Red 因为设置了 z-index = 1,并且是 absolute 定位,所以生成了层叠上下文,Red 会高于其他元素。

green 和 blue 都是非定位元素,按照出现顺序,blue 覆盖 green。

所以从底层到上边的顺序就是绿色、蓝色、红色。

image-20230622130720208

下边思考一下如果修改代码,并且在下边的限制条件下,让红色到最底层:

  • 不修改任何标签元素的名字,只增加修改 css
  • 不改变任何元素的 z-index
  • 不改变任何元素的 position 属性

如果直接知道答案了,那层叠关系应该是学透了。

答案就是给 div 加一个透明度:

1
2
3
div {
opacity: 0.99;
}

image-20230622131314901

我们重新分析一下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.red,
.green,
.blue {
position: absolute;
width: 100px;
color: white;
line-height: 100px;
text-align: center;
}

.red {
z-index: 1;
top: 20px;
left: 20px;
background: red;
}

.green {
top: 60px;
left: 60px;
background: green;
}

.blue {
top: 100px;
left: 100px;
background: blue;
}
div {
opacity: 0.99;
}
</style>
</head>
<body>
<div>
<span class="red">Red</span>
</div>
<div>
<span class="green">Green</span>
</div>
<div>
<span class="blue">Blue</span>
</div>
</body>
</html>

现在相当于有五个层叠上下文:

html(初始的一个层叠上下文)

  • div(通过 opacity 生成)
    • red(通过 absolute + z-index 生成)
  • div(通过 opacity 生成)
  • div(通过 opacity 生成)

比较 Red、Green、Blue 的层叠顺序,就是比较三者所在的层叠上下文,即各自所在的 div,三个 div 都是通过 opacity 生成的层叠上下文,所以它们层叠顺序就是出现的顺序,从底部到顶层就是 Red、Green、Blue。

即使 Green 和 Blue 本身没有生成层叠上下文,但因为它们所在的父元素的层叠上下文比较高,所以就把 Red 覆盖了。

再举个例子,因为比较的是所在的层叠上下文的顺序,因此平常开发中会遇到设置 z-index = 999(同时是定位元素了),也无法到最上层。原因就是它所在的层叠上下文比较低,类似于下边的情况。

image-20230622140148429

还有一个神奇的现象:

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
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.my-element {
background: rgb(232 240 254 / 0.5);
border: 1px solid lightblue;

width: 250px;
height: 250px;
}

.my-element .child {
position: relative;
z-index: -1;

background: pink;
border: 1px solid hotpink;
padding: 1rem;
width: 275px;
}
</style>
</head>
<body>
<div class="my-element">
<div class="child">I am behind my parent</div>
</div>
</body>
</html>

回忆下之前说的堆叠顺序:

image-20230622155424571

因为父元素和子元素都在同一个层叠上下文下,所以会先堆叠 z-index 为负值的元素,所以就形成了子元素穿越到父元素下边的情况。

image-20230622140741157

如果我们让父元素也生成一个层叠上下文,上边的情况就不会发生了:

1
2
3
4
5
6
7
8
9
.my-element {
position: relative;
z-index: 0;
background: rgb(232 240 254 / 0.5);
border: 1px solid lightblue;

width: 250px;
height: 250px;
}

image-20230622141026125

当父元素加了层叠上下文之后,父元素和子元素就不在同一层叠上下文中了。

父元素在根元素上。

子元素在父元素上。

堆叠顺序判断

总结一下:

判断元素之间的堆叠顺序,首先判断是否在同一层叠上下文中。

如果在同一堆叠上下文,就按照下边的顺序:

  1. 非定位的 block 元素,一般就是背景
  2. float 元素
  3. 非定位的 inline 元素,一般就是文字内容
  4. 定位元素,即 position 设置了 relative 或者 absolute

如果不在同一堆叠上下文,就找到元素所在的层叠上下文,并且要一直往上找层叠上下文,直到找到从它们共同层叠上下生成的那个层叠上下文:

按照下边的规则判断层叠上下文的顺序,层叠上下文的顺序就是要比较元素的堆叠顺序了:

  1. 通过 z-index 加上某些条件生成的层叠上下文,并且 z-index 为负值
  2. 没有生成层叠上下文的元素,即之前讨论的无新增层叠上下文的情况
    1. 非定位的 block 元素,一般就是背景
    2. float 元素
    3. 非定位的 inline 元素,一般就是文字内容
    4. 定位元素,即 position 设置了 relative 或者 absolute,但没设置 z-index
  3. 通过 z-index 加上某些条件生成的层叠上下文,并且 z-index 为 0 或者其他条件生成的层叠上下文
  4. 通过 z-index 加上某些条件生成的层叠上下文,并且 z-index 为正值,值越大越在上边。

实践经验

能不设置 z-index 就不要去设置,设置请三思。

定位元素天生高于普通元素

设置了 relative 或者 absolute 的元素会高于其他元素,因此这种情况下完全可以不设置 z-index,如果设置了 z-index 就会生成新的层叠上下文,可能会造成堆叠的混乱。

另外因为设置了 fixed 即使不设置 z-index 也会生成一个层叠上下文,因此 fixed 元素会高于其他所有的普通元素(定位元素和非定位元素)。但如果页面中有定位元素设置了正的 z-index,就不得不给 fixed 元素加一个更大 z-index 了。

子元素层级受到父层叠上下文的影响

当设置了一个 z-index 产生了层叠上下文后,需要考虑当前元素会不会成为别的元素的父元素,如果在多人合作中经常互相改代码或者引用组件,如果某个地方产生了层叠上下文,那子元素的层级就会受到该父元素的影响从而导致达不到想要的层级。

比如将一个弹窗组件放到了一个父元素中,父元素有层叠上下文,这样就会导致弹窗组件达不到自己想要的高度。

z-index 管理思考

团队中一个项目过大之后,层级问题真的是防不胜防,也许可以做下边的事情来降低问题的发生:

宣导

因为层级和 z-index 的问题可能没详细去了解过,边开发边调试最后达到效果就好。所以最好可以先宣导一下,把层级的问题团队内完全对齐,降低问题的发生。

开发前

设计一套体系来管理 z-index。

常规的做法就是将所有的 z-index 定义为变量统一管理,并且规定范围,普通元素 1 - 100,弹窗 101 - 999 类似这样。

当有页面需要 z-index 时就去注册,命名的时候可以按页面、按组件范围进行区分,这样未来想知道某个页面有哪些 z-index 可以一目了然。

开发中

规则有了,但不遵守没啥用。

需要在 commit 以及打包流水线中进行强制卡控,如果发现 z-index 使用了数字就禁止提交 commit,如果强制用 -n 提交了,就在流水线中禁止打包。

老项目

对于老项目去推动上边的流程真的太难了,把所有的 z-index 去重新定义变量,对于大项目来说修改、回归工作量会很大很大,因此基本无望。

可以做点工具来尽量避免出现层级的问题:

比如页面的层叠上下文进行静态扫描,可以把层叠上下文的关系展示出来,这样如果需要新加层叠上下文,可以直观的知道会不会影响到别人。

再进一步,如果有全套的 Mock 数据,可以模拟出来所有层叠上下文都渲染时候,真实页面长什么样子,会更加直观。

ps

以上思考都是理想情况下可以做的事情,现实状况可能会遇到小团队没必要推,大团队推不动的情况,哈哈。

windliang wechat