CSS 层叠相关知识指北

亲爱的观众老爷们大家好~我发现我写文章都是因为工作碰到问题才写,什么探索最前沿的最讨厌了(其实是力有所不及)!言归正传,最近碰到的问题是这个:准备开发一个平台,随手写导航栏组件之时,发现层级错乱,无论如何调z-index都无法达到预想的效果,大致代码如下:

<nav>
    //背景遮盖,绝对定位
    <section class="mask"></section>
    <ul>
        <li>
            balabala..
            //次级列表,绝对定位
            <ul class="menu">
                <li>1</li>
                <li>2</li>
                <li>3</li>
            </ul>
        </li>
    </ul>
</nav>

大致想要的效果就和这平台的导航差不多。CSS代码就不贴了,我写的时候碰到的问题就是无法将mask绝对定位后的层级置于li文本之下,又在使用了最对定位的menu之上。当时为了赶需求,征询了产品的意见,改了实现的方式。但本着碰到问题不回避的态度,通过两天闲时废(shi)寝(mian)忘(duo)食(meng)地查阅资料与疯狂写demo后,小结了点知识,在此分享给大家,希望对大家有所帮助。

本文主要是关于层叠上下文和层叠顺序的相关知识,如果看官大人已经通晓,可能这篇文章帮不了你什么,但你能帮我看下写得有木有问题(手动滑稽)。

(友情提示,我写这篇文章时,为了让看官们更好地理解,写得挺罗嗦的,如果直接想看结论,可以跳到最后~)

层叠上下文

先来点简单的,我们先看”stacking context”,也就是层叠上下文。

层叠上下文是HTML元素的三维概念,这些HTML元素在一条假想的相对于面向(电脑屏幕的)视窗或者网页的用户的z轴上延伸,HTML元素依据其自身属性按照优先级顺序占用层叠上下文的空间。

上面的描述来自于MDN,简单地说,就是几个元素叠在一起,哪个放在“上面”。一般而言,某个元素一旦生成了层叠上下文,它会置于其他元素的上方,但这并不是绝对的,暂时请先记住这点。

根据MDN文档与自我试验,生成层叠上下文的方式有以下几种:

  • 根元素 (HTML),
  • z-index 值不为 “auto”的 绝对/相对定位,
  • 一个 z-index 值不为 autoflex 项目 (flex item),即:父元素 display: flex|inline-flex;
  • opacity 属性值小于 1 的元素,
  • transform 属性值不为 none 的元素,
  • mix-blend-mode 属性值不为 normal 的元素,
  • filter值不为 none 的元素,
  • perspective值不为 none 的元素,
  • isolation 属性被设置为 isolate 的元素,
  • position: fixed
  • will-change 中指定了任意 CSS 属性,即便你没有直接指定上述属性的值,
  • -webkit-overflow-scrolling 属性被设置 touch 的元素。

代码描述如下:

<!--HTML结构-->
<div class="div1"></div>
<div class="div2"></div>

/*css代码*/
div {
  width: 100px;
  height: 100px;
}

.div1 {
  background: red;
}

.div2 {
  background: blue;
  margin-top: -50px;
}

这个时候画风是这样的:

没毛病吧?

跟着你往div1的CSS中加入上述随意一个条件之一,比如opacity: .9;,画风就会变成:

也就是说由于红色的div生成了层叠上下文,从原来置于蓝色的div下方变为上方了。其他条件各位看官可以自行实现。

层叠水平

看起来还是蛮简单的对吧?那现在来理解下一个概念:层叠水平。每个元素其实都有自己的层叠水平,不单是受z-index影响的元素,不同元素之间其实是通过对比层叠水平来确定哪个在上面的。然而,层叠上下文除了让元素的层级更高之外,还会生成一个独立的上下文,它的子元素的层叠水平只在当前上下文生效。

简单说,你要确定两个元素哪个在上面,要先确定它们是否在同一个层叠上下文中,如果不在同一个上下文,那就找到在同一层叠上下文的祖先元素去“拼爹”。层叠水平的对比只在同一层叠上下文中才有意义。

看着还是蛮复杂,上代码感受下可能更清楚:

<!--HTML结构-->
<div class="div1">
    <div class="div1Child"></div>
</div>
<div class="div2">
    <div class="div2Child"></div>
</div>

/*css代码*/
.div1 {
  /*isolation: isolate;*/
}

.div1Child {
  background: red;
  position: absolute;
  z-index: 10;
}

.div2 {
  margin-top: -50px;
  /*isolation: isolate;*/
}

.div2Child {
  background: blue;
  position: absolute;
  z-index: 1;
}

先来这样的代码,复制到浏览器跑一下,可以看出是红色在上面的。这很科学,因为红色的z-index更大嘛。跟着解除对isolation: isolate;的注释,可以看到蓝色跑到红色上面去了。那是因为.div1.div2都生成了层叠上下文,它们的子元素z-index再大也不会作用于上下文以外的元素。但如果子元素位置重叠了,那怎么确定哪个在上面呢?那就去找它们的爸爸,直到找到处于同一层叠上下文(此例中的上下文是根元素形成的)的祖先元素(此例中是.div1.div2),让两个祖先元素对比一下哪个位于上面就好了。不同层叠上下文的子元素进行对比一定是通过“拼爹”来确定的。

当然,也存在不是“拼爹”的情况,看看这么一种情景:

<!--HTML结构-->
<div class="div1">
    <div class="div1Child"></div>
</div>
<div class="div2">
    <div class="div2Child"></div>
</div>

/*css代码*/
.div1 {
  /*没有形成层叠上下文*/
  /*isolation: isolate;*/
}

.div1Child {
  background: red;
  position: absolute;
  z-index: 10;
}

.div2 {
  margin-top: -50px;
  /*形成层叠上下文*/
  isolation: isolate;
}

.div2Child {
  background: blue;
  position: absolute;
  /*改动*/
  z-index: 100;
}

思考一下这时候谁在上面?答案是红色在上面。这是由于.div1没有形成层叠上下文,也就意味着.div1Child形成了自己的层叠上下文,而且是在根元素的层叠上下文中起作用的,而.div2也形成了自己的层叠上下文,所以.div2Child不与外面的元素作对比层叠水平。此时上下关系对比的是.div1Child.div2在根元素层叠上下文中层叠水平的对比。所以,要确认两个元素哪个在上面,需要把它们拉到最近的一个层叠上下文中,和函数的作用域类似,只能向上找,不能往下找。

好像说得不太清楚,那就来个不恰当的比喻吧。想象整个根元素是一个大箱子,里面有各种杂七杂八的子孙元素,但他们没装箱。它的每一个子孙元素,一旦形成了层叠上下文,那么连上它的子元素,都就会被装入一个小一点箱子(上述过程可以无限次执行,小箱子中有元素形成了层叠上下文,会独立包成一个更小的箱子)。而且这些箱子都有黑科技加持,无论里面的东西(层叠水平)多高,箱子看上都都是扁扁的(影响不了箱子外面)。同一个箱子内,哪个东西放最上面,那就看它的层叠水平咯。不同箱子中的东西想对比吗?也可以啊,不过只能通过比较两个箱子哪个放得高来决定了。希望这个比喻能帮你更好地理解上述概念。

暂时来说,我们可以得到的结论有这么几条:

  • 通过添加某些CSS条件,可以形成层叠上下文。
  • 形成层叠上下文的元素,层级高于其他元素。
  • 层叠水平的对比,在相同的层叠上下文下才有意义。

有了上面的铺垫,下面将迎来重头戏:在同一层叠上下文中,不同箱子是按照什么规则进行摆放的呢?就只有z-index会产生影响吗?箱子内杂七杂八那些东西,就没个摆放顺序吗?

层叠顺序

最后就是重头戏啦,各种东西怎么摆放的,都是有规则的,这就是层叠顺序了。

先不谈其他规则,其实根据之前的例子,已经能总结出最基本的规则:如果两个元素在层叠顺序中所在的位置一样,那么后来者居上。

比如这个:

<!--HTML结构-->
<div class="div1"></div>
<div class="div2"></div>

/*css代码*/
.div1 {
  isolation: isolate;
  background: red;
}

.div2 {
  margin-top: -50px;
  isolation: isolate;
  background: blue;
}

蓝色在上面,简单易懂清晰明了~

而另一条基本规律,z-index生效的情况下,值更大的排在上面,不用我贴代码了吧?相信看官大人已经实现了无数次这样的场景了。

然而,思考这个如何:

<!--HTML结构-->
<div class="div1"></div>
<div class="div2"></div>

/*css代码*/
.div1 {
  isolation: isolate;
  background: red;
}

.div2 {
  margin-top: -50px;
  /*改变*/
  position: relative;
  background: blue;
}

还是蓝色在上面哦!会不会觉得有点诧异呢?还记得我之前提到的:“一般而言,某个元素一旦生成了层叠上下文,它会置于其他元素的上方,但这并不是绝对的”这句话么?这就是体现!结合刚才的规律,只有两个情况可以解释这个现象,要么.div1.div2处于同一位置,因此后来居上。要么后者z-index高于前者,然而我们没有设置z-index,也就是说不可能出现这情况。所以,规律是:某个元素形成了层叠上下文,那么它在层叠顺序中的位置与z-index0或者auto的元素相同。

然而这里其实有点小坑的,我就掉进去了,也希望大家掉进去一次再爬出来,请看:

<!--HTML结构-->
<div class="div1">
    <div class="div1Child"></div>
</div>
<div class="div2"></div>

/*css代码*/
.div1 {
  background: red;
  position: relative;
}

.div1Child {
  background: yellow;
  position: relative;
  z-index: 1;
}

.div2 {
  margin-top: -50px;
  isolation: isolate;
  background: blue;
}

你觉得什么颜色会在最上面呢?

其实是黄色。依次的话是黄色->蓝色->红色。有没有同学认为,既然你说某个元素形成了层叠上下文,那么它在层叠顺序中的位置与z-index0或者auto的元素相同;而且如果两个元素在层叠顺序中所在的位置一样,那么后来者居上。因此应该蓝色最上面?我的傻孩子啊,其实你也对,你看,蓝色不久在红色上面吗?完美对应这条规则。然而.div1Child形成了自己的层叠上下文,是一个独立的“小箱子”啊,在同一层叠上下文中,z-index生效的情况下,值更大的排在上面,对吧?

踩进去又跳出来之后,后面的事情好办不少了,考虑到篇幅问题,直接给大家说“箱子”内没形成箱子的顺序吧。

依次是display: line-block|inline|flex的元素 -> 浮动元素 -> 块状元素 -> z-index小于0的元素。

因而,结合全部规则,总体的排序如下面的代码实现(渣排版见谅):

<!--HTML结构-->
<div class="zIndexMoreThanZero">
    <p>z-index`大于0的元素</p>
</div>
<div class="zIndexZero">
    <p>等价于或本身就是`z-index`等于`0`或者`auto`的元素</p>
</div>
<div class="inlineBlock">
    <p>display: line-block|inline|flex 的元素</p>
</div>
<div class="float">
    <p>浮动元素</p>
</div>
<div class="block">
    <p>块状元素</p>
</div>
<div class="zIndexLessThanZero">
    <p>z-index 小于0的元素</p>
</div>

/*css代码*/
div {
  width: 300px;
  height: 100px;
  margin-top: -30px;
  box-shadow: 3px 3px 1px #999;
}

div p {
  padding-top: 60px;
  font-size: 8px;
}

.zIndexMoreThanZero {
  margin-top: 0;
  position: relative;
  z-index: 1;
  background: lightblue;
}

.zIndexZero {
  isolation: isolate;
  background: lightcoral;
}

.inlineBlock {
  margin-left: -300px;
  display: inline-block;
  background: lightcyan;
}

.float {
  margin-top: 40px;
  float: left;
  background: lightgoldenrodyellow;
}

.block {
  margin-top: 40px;
  background: lightgray;
}

.zIndexLessThanZero {
  position: relative;
  z-index: -1;
  background: lightgreen;
}

浏览器页面如图:

事实上,还有最后一种顺序的,有比z-index小于0的元素更低的情况,那就是某元素形成了层叠上下文,它自己在自己形成的层叠上下文中,就是个垫底的存在了。代码例子如下:

<!--HTML结构-->
<div class="div1">
    <div class="div1Child"></div>
</div>

/*css代码*/
.div1 {
  background: red;
  position: relative;
  z-index: 10;
}

.div1Child1 {
  background: yellow;
  position: relative;
  z-index: -1;
  margin-left: 50px;
}

结果是黄色在上的。z-index只作用于元素所处的层叠上下文,不作用于自己形成的层叠上下文。

至此,我了解到关于CSS 层叠相关的知识,就全部分享给大家啦。

小结

文章太长,直接看这其实也行,小结下来,关于CSS 层叠相关知识的规矩有这么几个:

  • 由于某些CSS条件,会生成层叠上下文。
  • 所有元素都有它的层叠水平,而层叠水平的对比只在同一层叠上下文中才有意义。
  • 处于相同层叠顺序的,后来居上,不相同的按序排放。

事实而言,相关的知识倒不是特别重要,就算不懂也不会导致无法交功课的状况。只是,不希望自己把回避问题变为习惯,而且了解下来还是蛮有趣的,之后再出现类似的状况时,不至于束手无策。

感谢各位看官看到这里,文章太长了~希望对大家有所帮助,我对层叠水平与层叠顺序感觉不是特别透彻。如有不当之处,还请不吝赐教!

参考资料

深入理解CSS中的层叠上下文和层叠顺序

CSS层叠

The stacking context