深入理解 CSS 中的层叠上下文和层叠顺序
什么是层叠上下文
层叠上下文,英文称作”stacking context”. 是 HTML 中的一个三维的概念。如果一个元素含有层叠上下文,我们可以理解为这个元素在 z 轴上就“高人一等”。
这里出现了一个名词-z 轴,指的是什么呢?
z 轴表示的是用户与屏幕的这条看不见的垂直线。层叠上下文是一个概念,跟「块状格式化上下文(BFC)」类似。页面中的元素有了层叠上下文,则此元素级别更高,离我们用户更近了。
层叠上下文元素有如下特性,这些特性的具体表现我们后文会说明:
- 层叠上下文的层叠水平要比普通元素高;
- 层叠上下文可以阻断元素的混合模式;
- 层叠上下文可以嵌套,内部层叠上下文及其所有子元素均受制于外部的层叠上下文。
- 每个层叠上下文和兄弟元素独立,也就是当进行层叠变化或渲染的时候,只需要考虑后代元素。
- 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中。
什么是层叠水平
再来说说层叠水平。“层叠水平”英文称作”stacking level”,决定了同一个层叠上下文中元素在 z 轴上的显示顺序。
所有的元素都有层叠水平,包括层叠上下文元素,普通元素的层叠水平优先由层叠上下文决定,因此,层叠水平的比较只有在当前层叠上下文元素中才有意义。
需要注意的是,诸位千万不要把层叠水平和 CSS 的 z-index 属性混为一谈。没错,某些情况下 z-index 确实可以影响层叠水平,但是,只限于定位元素以及 flex 盒子的孩子元素;而层叠水平所有的元素都存在。
什么是层叠顺序
再来说说层叠顺序。“层叠顺序”英文称作”stacking order”. 表示元素发生层叠时候有着特定的垂直显示顺序,注意,这里跟上面两个不一样,上面的层叠上下文和层叠水平是概念,而这里的层叠顺序是规则。
层叠顺序规则遵循下面这张图:
这里有几点需要主要:
- 位于最低水平的 border/background 指的是层叠上下文元素的边框和背景色。每一个层叠顺序规则适用于一个完整的层叠上下文元素。
- z-index:0 实际上和 z-index:auto 单纯从层叠水平上看,是可以看成是一样的。注意这里的措辞——“单纯从层叠水平上看”,实际上,两者在层叠上下文领域有着根本性的差异。
大家有没有想过,为什么内联元素的层叠顺序要比浮动元素和块状元素都高?为什么呢?我明明感觉浮动元素和块状元素要更屌一点啊。
诸如 border/background 一般为装饰属性,而浮动和块状元素一般用作布局,而内联元素都是内容。网页中最重要的是什么?当然是内容。
因此,一定要让内容的层叠顺序相当高,当发生层叠是很好,重要的文字啊图片内容可以优先暴露在屏幕上。
层叠准则
下面这两个是层叠领域的黄金准则。当元素发生层叠的时候,其覆盖关系遵循下面 2 个准则:
- 谁大谁上:当具有明显的层叠水平标示的时候,如识别的 z-indx 值,在同一个层叠上下文领域,层叠水平值大的那一个覆盖小的那一个。通俗讲就是官大的压死官小的。
- 后来居上:当元素的层叠水平一致、层叠顺序相同的时候,在 DOM 流中处于后面的元素会覆盖前面的元素。
在 CSS 和 HTML 领域,只要元素发生了重叠,都离不开上面这两个黄金准则。
层叠上下文的创建
如同块状格式化上下文,层叠上下文也基本上是由一些特定的 CSS 属性创建的。有三种途径会创建层叠上下文:
- 页面根元素天生具有层叠上下文,称之为“根层叠上下文”。
- z-index 值为数值的定位元素的传统层叠上下文。
- 其他 CSS3 属性
根层叠上下文
指的是页面根元素,也就是滚动条的默认的始作俑者 html 元素。这就是为什么,绝对定位元素在 left/top 等值定位的时候,如果没有其他定位元素限制,会相对浏览器窗口定位的原因。
定位元素与传统层叠上下文
对于包含有 position:relative/position:absolute 的定位元素,当其 z-index 值不是 auto 的时候,会创建层叠上下文。
这里提一下 position:fixed, 在过去,position:fixed 和 relative/absolute 在层叠上下文这一块是一路货色,都是需要 z-index 为数值才行。但是,不知道什么时候起,Chrome 等 webkit 内核浏览器,position:fixed 元素天然层叠上下文元素,无需 z-index 为数值。经验证,在 Chrome(67.0.3396.99)下不需要声明 z-index(即 z-index 为 auto)也会创建层叠上下文。
我们看如下 HTML 代码:
<div style="position:relative; z-index:auto;">
<img src="mm1.jpg" style="position:absolute; z-index:2;"> <-- 横妹子 -->
</div>
<div style="position:relative; z-index:auto;">
<img src="mm2.jpg" style="position:relative; z-index:1;"> <-- 竖妹子 -->
</div>
大家会发现,竖着的妹子(mm2)被横着的妹子(mm1)给覆盖了。
下面,我们对父级简单调整下,把 z-index:auto 改成层叠水平一致的 z-index:0, 代码如下:
<div style="position:relative; z-index:0;">
<img src="mm1.jpg" style="position:absolute; z-index:2;"> <-- 横妹子 -->
</div>
<div style="position:relative; z-index:0;">
<img src="mm2.jpg" style="position:relative; z-index:1;"> <-- 竖妹子 -->
</div>
大家会发现,结果反过来了,竖着的妹子(mm2)这回趴在了横着的妹子(mm1)身上。
这两者的差别就在于,z-index:0 所在的 div 元素是层叠上下文元素,而 z-index:auto 所在的 div 元素是一个普通的元素,于是,里面的两个 img 妹子的层叠比较就不受父级的影响,两者直接套用层叠黄金准则,这里,两者有着明显不一的 z-index 值,因此,遵循“谁大谁上”的准则,于是,z-index 为 2 的那个横妹子,就趴在了 z-index 为 1 的竖妹子身上。
而 z-index 一旦变成数值,哪怕是 0,都会创建一个层叠上下文。此时,层叠规则就发生了变化。两个 img 妹子的层叠顺序比较变成了优先比较其父级层叠上下文元素的层叠顺序。这里,由于两者都是 z-index:0,层叠顺序这一块两者一样大,此时,遵循层叠黄金准则的另外一个准则“后来居上”,根据在 DOM 流中的位置决定谁在上面,于是,位于后面的竖着的妹子就自然而然趴在了横着的妹子身上。对,没错,img 元素上的 z-index 打酱油了!
CSS3 与新时代的层叠上下文
CSS3 的出现除了带来了新属性,对层叠上下文这一块有很大的影响。新增的一些 css3 属性会导致元素产生层叠上下文,我们整理如下:
- z-index 值不为 auto 的 flex 项(父元素 display:flex、inline-flex).
- 元素的 opacity 值不是 1.
- 元素的 transform 值不是 none.
- 元素 mix-blend-mode 值不是 normal.
- 元素的 filter 值不是 none.
- 元素的 isolation 值是 isolate.
- ill-change 指定的属性值为上面任意一个。
- 元素的-webkit-overflow-scrolling 设为 touch.
我们这里带大家验证几个比较常用的属性。
display:flex|inline-flex 与层叠上下文
要满足两个条件才能形成层叠上下文:条件 1 是父级需要是 display:flex 或者 display:inline-flex 水平,条件 2 是子元素的 z-index 不是 auto,必须是数值。此时,这个子元素为层叠上下文元素,没错,注意了,是子元素,不是 flex 父级元素。
我们来验证一下:
.box {}
.box > div { background-color: blue; z-index: 1; } /* 此时该div是普通元素,z-index无效 */
.box > div > img {
position: relative; z-index: -1; right: -100px; /* 注意这里是负值z-index */
}
<div class="box">
<div>
<img src="mm1.jpg">
</div>
</div>
我们发现妹子被蓝色背景挡住了:
为什么呢?层叠顺序图可以找到答案,如下:
从上图可以看出负值 z-index 的层叠顺序在 block 水平元素的下面,而蓝色背景 div 元素是个普通元素,因此,妹子直接穿越过去,在蓝色背景后面的显示了。
现在,我们 CSS 微调下,增加 display:flex, 如下:
.box { display: flex; }
.box > div { background-color: blue; z-index: 1; } /* 此时该div是层叠上下文元素,同时z-index生效 */
.box > div > img {
position: relative; z-index: -1; right: -150px; /* 注意这里是负值z-index */
}
结果如下:
会发现,妹子在蓝色背景上面显示了,为什么呢?还是根据上面的层叠顺序图可以看出负值 z-index 的层叠顺序在当前第一个父层叠上下文元素的上面,而此时,那个 z-index 值为 1 的蓝色背景 div 的父元素的 display 值是 flex,变成层叠上下文元素了。于是,图片在蓝色背景上面显示了。这个现象也证实了层叠上下文元素是 flex 子元素,而不是 flex 容器元素。
opacity 与层叠上下文
我们直接看代码:
<div class="box">
<img src="mm1.jpg">
</div>
.box { background-color: blue; }
.box > img {
position: relative; z-index: -1; right: -150px;
}
结果如下,和上面例子一样:
然后加个透明度,例如 50%透明:
.box { background-color: blue; opacity: 0.5; }
.box > img {
position: relative; z-index: -1; right: -150px;
}
结果如下:
原因就是半透明元素具有层叠上下文,妹子图片的 z-index:-1 无法穿透,于是,在蓝色背景上面乖乖显示了。
transform 与层叠上下文
应用了 transform 变换的元素同样具有层叠上下文。
我们直接看应用后的结果,如下 CSS 代码:
.box { background-color: blue; transform: rotate(15deg); }
.box > img {
position: relative; z-index: -1; right: -150px;
}
结果如下,妹子同样在蓝色背景之上:
filter 与层叠上下文
此处说的 filter 是 CSS3 中规范的滤镜,不是旧 IE 时代私有的那些,虽然目的类似。通常用来实现图片的灰度或者毛玻璃效果等。
我们使用常见的模糊效果示意下:
.box { background-color: blue; filter: blur(5px); }
.box > img {
position: relative; z-index: -1; right: -150px;
}
妹子蓝色床上躺着,只是你眼镜摘了,看得有些不够真切罢了:
层叠上下文与层叠顺序
本文多次提到,一旦普通元素具有了层叠上下文,其层叠顺序就会变高。那它的层叠顺序究竟在哪个位置呢?
这里需要分两种情况讨论:
- 如果层叠上下文元素不依赖 z-index 数值,则其层叠顺序是 z-index:auto;
- 如果层叠上下文元素依赖 z-index 数值,则其层叠顺序由 z-index 值决定。
正如我们的层叠顺序图所示:
大家知道为什么定位元素会层叠在普通元素的上面吗?
其根本原因就在于,元素一旦成为定位元素,其 z-index 就会自动生效,此时其 z-index 就是默认的 auto,根据上面的层叠顺序表,就会覆盖 inline 或 block 或 float 元素。
而不支持 z-index 的层叠上下文元素天然 z-index:auto 级别,也就意味着,层叠上下文元素和定位元素是一个层叠顺序的,于是当他们发生层叠的时候,遵循的是“后来居上”准则。
我们可以测试下:
<img src="mm1" style="position:relative">
<img src="mm2" style="transform:scale(1);">
反过来测试:
<img src="mm2" style="transform:scale(1);">
<img src="mm1" style="position:relative">
结果:
会发现,两者样式一模一样,仅仅是在 DOM 流中的位置不一样,导致他们的层叠表现不一样,后面的妹子趴在了前面妹子的身上。这也说明了,不依赖 z-index 数值的层叠上下文元素的层叠顺序就是 z-index:auto 级别。
结束语
只要元素发生层叠,要解释其表现,基本上就本文的这些内容了。
我发现很多小伙伴都有 z-index 滥用,或者使用不规范的问题。我觉得最主要的原因还是对层叠上下文以及层叠顺序这些概念都不了解。例如,只要使用了定位元素,尤其 absolute 绝对定位,都离不开设置一个 z-index 值;或者只要元素被其他元素覆盖了,例如变成定位元素或者增加 z-index 值升级。页面一复杂,必然搞得乱七八糟。
实际上,在很多常见情况下,z-index 根本就没有出现的必要。比如知道了内联元素的层叠水平比块状元素高,某条线你想覆盖上去的时候,需要设置 position:relative 吗?不需要,inline-block 化就可以。