原文地址

前言

最近在使用 CSS3 動畫的時候遇到一個 DOM 層疊的問題,故此重新學習了一下 z-index,感覺這個 CSS 屬性還是挺複雜的,希望本文可以幫助你重新認識 z-index 的魅力。

事情的經過是這樣的(背景有點長),最近在寫下面這樣的列表頁:

深入理解 CSS 屬性 z-index

然後給每個產品項新增一個 CSS3 動畫,動畫效果大概像這樣:demo 地址

實現後的效果大概是這樣的(截圖有點糊,建議點 demo 地址檢視):

深入理解 CSS 屬性 z-index

在 Chrome 上顯示正常,但是從 Safari 開啟,就發現不得了了,動畫十分卡頓:

深入理解 CSS 屬性 z-index

在切換不同的產品項的時候會發現頁面動畫明顯示卡頓,想到這,其實這難不倒我,於是我就給每個產品項新增 3D 動畫硬體加速,方法也十分簡單,就像下面這樣:

item

{

transform

translateZ

0

);

/* 或者 will-change: transform; */

}

這是一種常見的硬體加速的最佳化方式, 如果不懂的話可以看這個:用CSS開啟硬體加速來提高網站效能

之後開啟 Safari 後發現頁面動畫十分流暢,硬體加速的最佳化成功,但是隨之而來又出現新的問題,也就是本文所說的 DOM 元素層疊問題。

深入理解 CSS 屬性 z-index

雖然動畫效果卡頓修復了,但是頁面 DOM 元素層疊卻出現問題:也就是下面的產品項會覆蓋上面產品項右下角的入口彈框,而我們希望的正常的效果應該是這樣:

深入理解 CSS 屬性 z-index

遇到這樣的問題,第一反應:那我將彈框的 z-index 調大不就好了,小菜一碟,但是無論我怎麼調整 z-index 的值,彈框始終被下方的產品項所覆蓋,一開始我也百思不得其解。

最後透過看了一些資料學習梳理,最終找到解決的辦法,廢話不多說,下面我就開始梳理整個 z-index 相關知識,並在最後提交上述問題的解決方案。(

下文講解會附帶很多的用例,我將程式碼全部貼在 jsfiddle 方便查閱,讀者可以點選 demo 地址檢視例子)

z-index 基礎

首先介紹一下 z-index,z-index 屬性是用來調整元素及子元素在 z 軸上的順序,當元素髮生覆蓋的時候,哪個元素在上面,哪個元素在下面。通常來說,z-index 值較大的元素會覆蓋較低的元素。

z-index 的預設值為 auto,可以設定正整數,也可以設定為負整數,如果不考慮 CSS3,只有定位元素(position:relative/absolute/fixed)的 z-index 才有作用,如果你的 z-index 作用於一個非定位元素(一些 CSS3 也會生效),是不起任何作用的。比如:demo 地址

深入理解 CSS 屬性 z-index

當你為 DOM 元素設定了定位後,該元素的 z-index 就會生效,預設為 auto,你可以簡單將它等同於 z-index: 0,比如:demo 地址,

也就是說,z-index 生效的前提條件是必須要設定定位屬性(或者一些 CSS3 屬性),才能夠生效。

深入理解 CSS 屬性 z-index

看完 demo 你可能會覺得納悶,為啥我單單隻設定了一個 position 屬性,沒設定 z-index 值,為啥紅色方格會覆蓋藍色方格,這裡就涉及到了 z-index 層疊水平的知識。

層疊水平(stacking level)

一個 DOM 元素,在不考慮層疊上下文的情況下,會按照層疊水平決定元素在 z 軸上的顯示順序,

通俗易懂地講,不同的 DOM 元素組合在一起發生重疊的時候,它們的的顯示順序會遵循層疊水平的規則,而 z-index 是用來調整某個元素顯示順序,使該元素能夠上浮下沉。

那麼層疊水平是什麼樣的呢?下面就是著名的 7 階層疊水平(stacking level)

深入理解 CSS 屬性 z-index

可以看出,層疊水平規範了元素重疊時候的呈現規則,有了這個規則,我們也就不難解釋為何之前例子中紅色方格會覆蓋藍色方格。

因為當你設定了 position: relative 屬性後,元素 z-index:auto 生效導致層疊水平提升,比普通內聯元素來的高,所以紅色方格會顯示在上方。

知道了層疊水平的規則後,下面我就舉幾個例子來說明:

inline/inline-block 元素高於浮動元素

首先是 inline/inline-block 元素高於浮動元素:demo 地址

深入理解 CSS 屬性 z-index

可以很清晰的看出文字(inline元素)覆蓋了圖片(浮動元素)。

inline/inline-block 元素高於 block 元素

demo 地址

深入理解 CSS 屬性 z-index

紅色方格(inline-block)覆蓋綠色方格(block),但是由於文字(display:block)屬於 inline 水平,與紅色方格(inline-block) 同級,遵循後來居上(接下來會解釋)原則,沒有被 inline-block 元素覆蓋。

元素層疊水平相當

那麼當兩個元素層疊水平相同的時候,這時候就要遵循下面兩個準則:

後來居上原則

誰 z-index 大,誰在上的準則

後來居上的原則:

後來居上準則就是說,當元素層疊水平相同的時候後面的 DOM 會覆蓋前面的 DOM 元素。這個很好理解,不過多解釋了。這也就是我們經常會看到為什麼後面的元素會覆蓋前面的元素。

正如前面看到的那個例子,由於文字(display:block)屬於 inline 水平,與紅色方格(inline-block) 同級,遵循後來居上(接下來會解釋)原則,沒有被 inline-block 元素覆蓋,這裡我就不另外貼例子來說明了。

誰 z-index 大,誰在上:

因為 z-index 的存在,導致元素在相同的層疊上下文中的順序是可以調整的,那麼在 z-index 負值和正值的範圍內,在這兩個區間內的話 DOM 元素的 z-index 值越大,顯示順序就會越靠前。

知道了層疊水平之後,基本上只要元素在同一個層疊上下文中的顯示順序就確定了,但是如果是在不同的層疊上下文中呢,又是如何顯示的呢?這個層疊上下文又是什麼意思?別急,接著往下看。

層疊上下文

層疊上下文,你可以理解為 JS 中的作用域,一個頁面中往往不僅僅只有一個層疊上下文(因為有很多種方式可以生成層疊上下文,只是你沒有意識到而已),在一個層疊上下文內,我們按照層疊水平的規則來堆疊元素。

介紹完層疊上下文的概念,我們先來看看哪些方式可以建立層疊上下文?

正常情況下,一共有三種大的型別建立層疊上下文:

預設建立層疊上下文

需要配合 z-index 觸發建立層疊上下文

不需要配合 z-index 觸發建立層疊上下文

一、預設建立層疊上下文

預設建立層疊上下文,只有 HTML 根元素,這裡你可以理解為 body 標籤。它屬於根層疊上下文元素,不需要任何 CSS 屬性來觸發。

二、需要配合 z-index 觸發建立層疊上下文

依賴 z-index 值建立層疊上下文的情況:

position 值為 relative/absolute/fixed(部分瀏覽器)

flex 項(父元素 display 為 flex|inline-flex),注意是子元素,不是父元素建立層疊上下文

這兩種情況下,需要設定具體的 z-index 值,不能設定 z-index 為 auto,這也就是 z-index: auto 和 z-index: 0 的一點細微差別。

前面我們提到,設定 position: relative 的時候 z-index 的值為 auto 會生效,但是這時候並沒有建立層疊上下文,當設定 z-index 不為 auto,哪怕設定 z-index: 0 也會觸發元素建立層疊上下文。

三、不需要配合 z-index 觸發建立層疊上下文

這種情況下,基本上都是由 CSS3 中新增的屬性來觸發的,常見的有:

元素的透明度 opacity 小於1

元素的 mix-blend-mode 值不是 normal

元素的以下屬性的值不是 none:

transform

filter

perspective

clip-path

mask / mask-image / mask-border

元素的 isolution 屬性值為 isolate

元素的 -webkit-overflow-scrolling 屬性為 touch

元素的 will-change 屬性具備會建立層疊上下文的值

介紹完如何建立層疊上下文概念以及建立方式後,需要說明的是,建立了層疊上下文的元素可以理解區域性層疊上下文,它隻影響其子孫代元素,它自身的層疊水平是由它的父層疊上下文所決定的。

比較兩個 DOM 元素顯示順序

接下來就來總結一下如何比較兩個 DOM 元素的顯示順序呢?

如果是在相同的層疊上下文,按照層疊水平的規則來顯示元素

如果是在不同的層疊上下文中,先找到共同的祖先層疊上下文,然後比較共同層疊上下文下這個兩個元素所在的區域性層疊上下文的層疊水平。

千言萬語濃縮於這兩句話中,但是裡面注意的點有很多,我們先來看第一點:

共同層疊上下文

如果是在相同的層疊上下文,按照層疊水平的規則來顯示元素,這個之前在介紹層疊水平的時候就已經介紹了,值得注意的是,父子關係的元素很可能在相同的層疊上下文,這種情況下元素的層級比較也是按照層疊水平的規則來顯示。

舉個例子:demo 地址

深入理解 CSS 屬性 z-index

。box 元素和其子元素 img 的比較:因為 img 和 。box 屬於相同的層疊上下文中,因為 img z-index 為 -1,所以下沉到父元素的下面,父元素覆蓋了圖片,但是 img 還是在 body 的背景色之上,因為遵循 7 階層疊水平,最底下一定會是層疊上下文(body 元素)的 background 或者 border。

但是如果我們讓 。box 元素建立區域性層疊上下文的時候就不一樣了,。box 元素和 img 元素的也是同處於相同層疊上下文,只不過上下文切換為 。box 建立的區域性層疊上下文。

demo 地址

深入理解 CSS 屬性 z-index

你會發現:img 元素覆蓋了 。box 的背景色,因為層疊上下文的背景色永遠是在最低下,層疊上下文由 body 元素變為了 。box 元素,但是如果是 。box 下的 span 元素和 img 元素的比較,inline 元素高於 z-index 為負值的元素,所以 2222 顯示在圖片之上。

透過這個例子是想說明,父子元素的層疊比較有可能父元素是區域性層疊上下文,也可能不是區域性層疊上下文,那麼就需要去尋找共同的層疊上下文。

不同的層疊上下文

這個就比較複雜了,可以總結成一句話:打狗還得看主人,下面讓我先畫了草圖來說明一下:

深入理解 CSS 屬性 z-index

頁面中常見的 DOM 樹大概是長這樣:

這裡 Root、ParentX、ChildX 均為層疊上下文元素,並非一定是 ABCD 的父元素

A 元素想跟 B 或者 ChildB 元素比較,很高興,它們屬於相同層疊上下文(ChildB)下,根據層疊水平去判斷就可以了

如果 A 元素想跟 C 或者 ChildA 比較,那就去找它們共同的祖先層疊上下文(ParentB),找到之後,就根據祖先層疊上下文下兩個元素所在的區域性層疊上下文比較層疊水平(這裡就是 ChildA 和 ChildB 去比較)

同理,如果 A 想跟 D 一決雌雄,那麼就去找祖先層疊上下文(Root),然後去比較 ParentA 和 ParentB 的層疊水平即可

是不是很簡單,下面再透過兩個簡單的小示例來說明一下:

示例一:demo 地址

深入理解 CSS 屬性 z-index

雖然 childA 的 z-index: 9999 非常大,但是在跟 parentB 或者 childB 比較的時候,它沒資格去比,只能讓它的老大 parentA 去比較,parentA 跟 parentB 一比較,才發現:媽呀,原來你的 z-index 為 2 比我還大,失敬失敬,所以 childA 和 parentA 只好乖乖呆在 parentB 底下。

如果我們將例子稍微改下,讓 parentA 不再建立新的層疊上下文元素:demo 地址

深入理解 CSS 屬性 z-index

當 parentA 不再建立層疊上下文之後,childA 想跟 childB 比較,就不再受限於 parentA,而是直接跟 parentB 直接比較(因為 childA 和 parentB 在同一個層疊上下文),顯然 childA 在最上方,這也就是 childA 覆蓋 parentB 的原因。

問題的解決方案

理論知識已經介紹完了,如果你理解了上面的理論,這個問題應該是小菜一碟,下面就來說說一開始問題的解決方案:

因為在每個產品項上添加了 transform: translateZ(0) 導致每一個產品項都建立了一個層疊上下文,根據前面提到規則,每個產品項裡面的 DOM 元素的都是相互獨立的,取決於每個產品項(每個區域性層疊上下文),又由於這些產品項的層疊水平一致(與 z-index: auto 相同),遵循後來居上原則,這才導致了後面的元素會去覆蓋前面的元素。舉個簡單的例子:demo 地址

深入理解 CSS 屬性 z-index

就像這樣,即使你在 child 上新增多大的 z-index 屬性都不會改變它的層疊水平,唯一的辦法就是改變 item 的 z-index 數值,由於我們覆蓋的部分比較特殊,僅僅只是彈框部分,而彈框部分預設是不顯示的,只有當滑鼠懸浮到入口的時候才會顯示,最簡單的方式就是,當滑鼠 hover 到 item 上的時候,將其 z-index 值變大即可,破壞後來居上的特性:demo 地址

最終簡化效果:

深入理解 CSS 屬性 z-index

最佳實踐

說到這其實可以結束了,我在學習的過程中,看了張鑫旭大佬之前錄的影片,他提出了一些最佳實踐,我覺得挺不錯的,這裡也簡單地介紹一下:

不犯二準則:對於非浮層元素,避免設定 z-index 值,z-index 值沒有任何道理需要超過 2

對於浮層元素,可以透過 JS 獲取 body 下子元素的最大 z-index 值,然後在此基礎上加 1 作為浮層元素的 z-index 值

對於非浮層元素,不要過多地去運用 z-index 去調整顯示順序,要靈活地去運用層疊水平和後來居上的準則去讓元素獲得正確的顯示,如果是在要設定 z-index 去調整,不建議非浮層元素 z-index 數值超過 2,對於 DOM 元素,-1, 0, 1, 2 足夠讓元素有正確的顯示順序。

對於浮層元素,往往是第三方元件開發,當你無法確認你的浮層是否會百分百覆蓋在 DOM 樹上的時候,你可以去動態獲取頁面 body 元素下所有子元素 z-index 的最大值,在此基礎加一作為浮層元素 z-index 值,用於保證該浮層元素能夠顯示在最上方。

結尾

最後的最後,本篇深入 z-index 屬性已經就完結了,感覺 CSS 屬性有許許多多的彩蛋,接下來有時間多接觸,多總結,有時間會繼續分享出來。

參考連結

CSS深入理解之z-index