CSS 層疊相關知識指北

sea_ljf發表於2017-09-08

親愛的觀眾老爺們大家好~我發現我寫文章都是因為工作碰到問題才寫,什麼探索最前沿的最討厭了(其實是力有所不及)!言歸正傳,最近碰到的問題是這個:準備開發一個平臺,隨手寫導航欄元件之時,發現層級錯亂,無論如何調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