層疊上下文 Stacking Context
在CSS2.1規范中,每個盒模型的位置是三維的,分別是平面畫布上的x軸,y軸以及表示層疊的z軸。對於每個html元素,都可以通過設置z-index屬性來設置該元素在視覺渲染模型中的層疊順序。
z-index可以設置成三個值:
auto,默認值。當設置為auto的時候,當前元素的層疊級數是0,同時這個盒不會創建新的層級上下文(除非是根元素,即<html>);<integer>。指示層疊級數,可以使負值,同時無論是什么值,都會創建一個新的層疊上下文;inherit。
除了由根根元素創建的根層疊上下文以外,其它上下文是由z-index不為auto的“positioned”元素所創建。
參考層疊級數,瀏覽器會根據以下規則來渲染繪制每個在同一個層疊上下文中的盒模型:
(從先繪制到后繪制)
- 創建層疊上下文的元素的背景和邊界;
z-index為負值的子元素,數值越小越早被繪制;- 同時滿足“in-flow”、“non-inline-level”、“non-positioned”的后代元素;
- “non-positioned”的浮動元素;
- 滿足“in-flow”、“inline-level”、“non-positioned”的后代元素;
- 層疊級數為0的子層疊上下文以及“positioned”且層疊級數為0的后代元素;
- 層疊級數大於等於1的“positioned”子層疊上下文,數值越小越早被繪制;
在規則中,提到了幾種元素的修飾詞,下面是簡單的解釋:
- “positioned”指的是
position為fixed,absolute,relative;那么如果未設置或為static的就是“non-positioned”元素; - “out-of-flow”元素指的浮動的或絕對定位(
fixed、absolute)的元素,又或者是根元素;如果不是上述情況,那個這個元素就是“in-flow”; - “inline-level”元素指的是
display為inline,inline-table,inline-block的元素;
規則有點多,但簡單說,就是父元素會先繪制,接着是z-index為負值的子元素,然后是“non-positioned”元素,最后是按照層疊級數從0開始逐級繪制(這樣說比較簡單,省略了大量細節,因此並不是很准確)。如果層級相同,則按照元素在DOM樹中的順序來進行繪制。
從這樣看,要讓z-index非負的元素按照層級控制生效,那么就將該元素設置為“positioned”,這也是許多文章中普遍提到的規則。
下面,將利用MDN中的例子來分析和解釋層疊上下文中的規則和計算方法,部分代碼使用的MDN上的源碼,另外一些是經過細微修改,目的是為了更好得把問題描述得更清楚。
不設置z-index的層疊
利用MDN上的一個例子來說明。
為了方便比較,將源碼簡化成如下:
<body>
<div id="absdiv1">DIV #1</div>
<div id="reldiv1">DIV #2</div>
<div id="reldiv2">DIV #3</div>
<div id="absdiv2">DIV #4</div>
<div id="normdiv">DIV #5</div>
</body>
其中DIV#1和DIV#4是粉色框,position設置為absolute;
DIV#2和DIV#3是粉色框,position設置為relative;
DIV#5是黃色框,position為設置,默認static;

根據規則,由於DIV#5是“non-positioned”,即使DIV#5是DOM樹中最后的元素,它也是最早被繪制的,因此它處於所有“positioned”的下面;而對於其余四個“positioned”的DIV,它們的繪制順序就是按照在DOM樹中的順序繪制,即DIV#1->DIV#2->DIV#3->DIV#4。
盡管DIV#5是最“先繪制”的,但是瀏覽器在解析HTML的時候仍然是按照HTML文檔流的順序來解析的,實際的繪制順序仍然是DIV#1->DIV#2->DIV#3->DIV#4->DIV#5。只不過,要繪DIV#5的時候,會對影響到的元素進行重新繪制,其渲染的效果看上去的順序是DIV#5->DIV#1->DIV#2->DIV#3->DIV#4,將DIV#5提到了最前。
float的層疊
同樣是要MDN上面的例子來說明。
<body>
<div id="absdiv1">
<br /><span class="bold">DIV #1</span>
<br />position: absolute;
</div>
<div id="flodiv1">
<br /><span class="bold">DIV #2</span>
<br />float: left;
</div>
<div id="flodiv2">
<br /><span class="bold">DIV #3</span>
<br />float: right;
</div>
<br />
<div id="normdiv">
<br /><span class="bold">DIV #4</span>
<br />no positioning
</div>
<div id="absdiv2">
<br /><span class="bold">DIV #5</span>
<br />position: absolute;
</div>
</body>
其中DIV#1和DIV#5是粉色框,position設置為absolute;
DIV#1和DIV#2是粉色框,float設置分別為left和right,opacity是1;
DIV#4是黃色框,position為設置,默認static;

上一節的例子類似,由於DIV#4是“non-positioned”,所以DIV#4仍然是最先繪制的,因此它的背景和邊界將在所有元素的最下面。而且根據規則,DIV#4中的inline-level元素(<span>)會在浮動元素繪制以后才繪制,結果是<span>被擠到了DIV#2的右邊。
根據規則,浮動元素是在“positioned”元素之前繪制,因此DIV#1和DIV#5會在兩個浮動元素的上面。
要注意到,在這里幾個<div>的並沒有設置透明度,這跟MDN上的源碼有所區別。那現在,如果完全按照MDN的源碼,將DIV#1,DIV#2,DIV#3,DIV#5的opacity設置為0.7,顯示結果如下:

仔細觀察,可以發現,在設置了opacity后,DIV#3的層級被提高到了DIV#1之上了。這與CSS2.1上的規定有所區別。
如果對DIV#4設置opacity:0.99,結果更加出人意料:

原本在最下面的DIV#4跑到了更加前面的位置,只位於DIV#5之下。
由於opacity並不是在CSS2.1里規定,需要使用CSS3中新的規則來解釋這一個現象,更容易理解z-index的規則,現在暫時不討論opacity所帶來的影響,避免把規則變得更復雜。
設置了z-index的層疊
再次使用MDN中的例子:
<body>
<div id="absdiv1">DIV #1</div>
<div id="reldiv1">DIV #2</div>
<div id="reldiv2">DIV #3</div>
<div id="absdiv2">DIV #4</div>
<div id="normdiv">DIV #5</div>
</div>
為了讓結構更加清楚,簡化了HTML源碼,下面是每個<div>的屬性設置:
DIV#1:position: absolute,z-index: 5;DIV#2:position: relative,z-index: 3;DIV#3:position: relative,z-index: 2;DIV#4:position: absolute,z-index: 1;DIV#5:position: static,z-index: 8;

又見到了可憐的DIV#5,盡管它的z-index:8是所有元素中最大的,但由於它是“non-posititoned”所以,它在層疊上還是地位低下,仍然要老老實實呆在其他元素的下面。
而對於其他“positioned”元素,它們的繪制順序就是按照z-index的大小來加以分別,因此盡管DIV#1在DOM樹中是最靠前的,但由於它的z-index: 5比其他都大,因此就成了最頂層的元素了。
層疊上下文
首先,回憶一下,創造層疊上下文的兩種情況:
- 根元素,創建根層疊上下文;
z-index不為auto的positioned元素;
實例一(同一層疊上下文中的時代)
繼續使用MDN上的例子,來說明如果層疊上下文對z-index計算的影響。
<body>
<div id="div1">
<div id="div2"></div>
</div>
<div id="div3">
<div id="div4"></div>
</div>
</body>
免去其他雜亂的樣式和顯示,HTML的主體結構如上所示,其中的屬性設置如下:
DIV#1:position: relative;DIV#2:position: absolute,z-index: 1;
DIV#3:position: relative;DIV#4:posititon: absolute;
從代碼就可以推斷出,除了根元素創建的根層疊上下文以外,還有DIV#2所創建的層疊上下文。因此,盡管DIV#2與DIV#3或DIV#4都不在一個BFC(塊格式化上下文)中,但它們都同處於一個層疊上下文中,因此根據層疊規則,DIV#2的z-index最高,因此處於另外三個元素之上。
顯示的結果則如下圖:

當然,如果將DIV#4設置z-index: 2,那么DIV#4就會跑到最頂部:

從此可以得知,層疊計算時,將考慮同一個層疊上下文中的所有元素而不考慮元素是否有其他聯系。
實例二(拼爹的時代)
依然上上面的例子:
<body>
<div id="div1">
<div id="div2"></div>
</div>
<div id="div3">
<div id="div4"></div>
</div>
</body>
但現在將各個元素的屬性做一些修改:
DIV#1:position: relative;DIV#2:position: absolute,z-index: 2;
DIV#3:position: relative,z-index: 1;DIV#4:posititon: absolute,z-index: 100;
在看結果之前,先根據源碼推斷一下計算的結果。首先,DIV#2創建了一個層疊上下文(SC2),而DIV#2本身在根層疊上下文中的層級是2;與DIV#2處於同一個層疊上下文的DIV#3也創建了一個層疊上下文(SC3),同時由於其z-index是1,比DIV#2要小,DIV#3理所當然地會屈於DIV#2之下;另外,DIV#3還有一個子元素DIV#4,DIV#4顯然是處於DIV#3所創建的層疊上下文(SC3)中,同時,自己又創建了另一個新的層級上下文(SC4)。
那么問題來了,DIV#4的z-index是100,比所有元素都要大,那么DIV#4會處於什么位置呢?

從結果可以看到,DIV#2和DIV#3位置和預想中是一樣的,但由於DIV#4則是處於DIV#2之下DIV#3之上。其中原因還,DIV#4所處的層疊上下文SC3的層級比SC2要低,因此不管DIV#4有多大,它都不會超過比自身高的層疊上下文中的元素。
如果改一改各個元素的屬性:
DIV#1:position: relative,z-index: 1;DIV#2:position: absolute,z-index: 100;
DIV#3:position: relative,z-index: 1;DIV#4:posititon: absolute,z-index: 2;
通過修改代碼,我們讓DIV#1和DIV#3的z-index為1,它們在SC0(根層疊上下文)中的層級都是1,那么它們將按照DOM樹的順序來繪制,這意味着DIV#3稍微比DIV#1高那么一點。
在這兩個層疊上下文中,分別有子元素DIV#2和DIV#4。此時,盡管DIV#2的層級數非常大,但由於它所處的層疊上下文SC1在SC3之下,因此DIV#2不僅在DIV#4之下,還會位於DIV#3之下。顯示結果如下圖所示:

通過這個例子,可以更清楚得認識到,層疊的計算是非常依賴所處的層疊上下文的,用剛通俗的話講,層疊計算時期是一個拼爹的時代。
小結
到這里,可以得到一些結論:
- 在同一個層疊上下文中計算層疊順序時,根據前文所提到的規則來進行就是;
- 對於不同的層疊上下文的元素,層級較大的層疊上下文中的元素用於處於層級小的層疊上下文中的元素之上(MG12將其歸結為從父規則);
- 從另一個角度理解,不同層疊上下文中的元素在計算層疊順序時不會互相影響,因為在層疊上下文被創建的時候它與其他上下文的層疊順序就早已經被決定了;
創建層疊上下文
前文曾經提到,根元素以及z-index非auto的“positioned”元素可以會創建新的層疊上下文,這也是CSS2.1規范唯一提到的,但是在CSS3中,創建層疊上下文的觸發條件有了修改,在MDN中有如下描述:
文檔中的層疊上下文由滿足以下任意一個條件的元素形成:
- 根元素 (HTML),
- 絕對(absolute)定位或相對(relative)定位且 z-index 值不為"auto",
- 一個 flex 項目(flex item),且 z-index 值不為 "auto",也就是父元素 display: flex|inline-flex,
- 元素的 opacity 屬性值小於 1(參考 the specification for opacity),
- 元素的 transform 屬性值不為 "none",
- 元素的 mix-blend-mode 屬性值不為 "normal",
- 元素的 isolation 屬性被設置為 "isolate",
- 在 mobile WebKit 和 Chrome 22+ 內核的瀏覽器中,position: fixed 總是創建一個新的層疊上下文, 即使 z-index 的值是 "auto" (參考 這篇文章),
- 在 will-change 中指定了任意 CSS 屬性,即便你沒有定義該元素的這些屬性(參考 這篇文章)
- 元素的 -webkit-overflow-scrolling 屬性被設置 "touch"
opacity的影響
在這里,我們看到了那個令人驚訝的opacity,原來它也創建了一個新的層疊上下文。為什么opacity小於1時需要創建新的層疊上下文呢?在CSS3-color中有這樣的解釋。
Since an element with opacity less than 1 is composited from a single offscreen image, content outside of it cannot be layered in z-order between pieces of content inside of it. For the same reason, implementations must create a new stacking context for any element with opacity less than 1.
由於一個opacity小於1的元素需要依靠這個元素以外的圖像來合成,因此它外部內容不能根據z-index被層疊到該元素的內容中間(子元素也會變得透明,如果存在z-index不為auto的“positioned”子元素,那么這些子元素就需要與外部元素進行層疊計算,透明部分就會有奇怪的計算結果),因此它需要創建一個新的層疊上下文,以防止外部內容對該元素的透明化內容造成影響。
那么opacity對實際的層疊會有什么影響呢?規范中這樣描述的:
If an element with opacity less than 1 is not positioned, implementations must paint the layer it creates, within its parent stacking context, at the same stacking order that would be used if it were a positioned element with ‘z-index: 0’ and ‘opacity: 1’. If an element with opacity less than 1 is positioned, the ‘z-index’ property applies as described in [CSS21], except that ‘auto’ is treated as ‘0’ since a new stacking context is always created. See section 9.9 and Appendix E of [CSS21] for more information on stacking contexts. The rules in this paragraph do not apply to SVG elements, since SVG has its own rendering model ([SVG11], Chapter 3).
opacity小於1的“non-positioned”元素,它就會被當作一個z-index: 0且opacity: 1的“positioned”元素一樣,來進行層疊計算(前文規則中的第6層);opacity小於1的“positioned”元素,它將按照前文中z-index的層疊規則計算技術,只不過,即使z-index是auto,仍然會創建層疊上下文;
回到之前討論“不設置z-index的層疊”時用到的例子:
<body>
<div id="flodiv2">DIV #1</div>
<div id="normdiv">DIV #2</div>
<div id="flodiv2">DIV #3</div>
<div id="normdiv">DIV #4</div>
<div id="absdiv2">DIV #5</div>
</body>
將DIV#3的opacity設置為0.7,顯示結果如下:

所有的opacity小於1的元素都是“positioned”,z-index默認為auto,即為0,根據規則6(層疊級數為0的子元素以及“positioned”且層疊級數為0的后代元素),它將不是浮動元素,而是一個“positioned”且層疊級數為0的元素,因此它將會被繪制到DIV#1之上(如果opacity為1,它應該是在DIV#1之下的);
如果僅將DIV#4設置opacity: 0.9,那么結果會使:

那么DIV#4就是opacity小於1的non-positioned元素,它將同樣被當成z-index: 0且opacity: 1 的 “positioned”元素一樣,即是規則6(層疊級數為0的子元素以及“positioned”且層疊級數為0的后代元素),由於它與其他元素都處於z-index: 0,因此根據DOM樹的順序,它將僅在DIV#5之下。(即使將其他所有元素都設置opacity小於1,那么所有的這些元素都是根據規則6進行層疊計算,那么結果就是根據DOM樹順序產生)
Problem solved!!!
至於其他觸發條件,就不再一一分析了。
總結
- 元素設置了
z-index后,必須將position設置為fixed、absolute或relative才回使z-index創建新的層疊上下文或生效; - 根元素(
<html>)擁有一個根層疊上下文; - 計算層疊順序時,需要先考慮元素所處的層疊上下文,層疊上下文之間的層疊關系直接決定了其元素集合之間的層疊關系(從父規則);
opacity及一些其他新的CSS3屬性的設置也可能創建新的層疊上下文,這些屬性的引入讓層疊計算變得更加復雜;- 層疊計算規則基本是(不是最准確的描述):
- 創建層疊上下文的元素的背景和邊界;
z-index為負值的子元素;- “non-positioned”的元素;
- “non-positioned”的浮動元素;
- “non-positioned”的內聯元素(文本等);
z-index為0的“positioned”元素;z-index大於等於1的“positioned”子元素;
層疊上下文是個比較少接觸的概念,但這又是一個非常重要的概念,它決定了元素的層疊順序的計算方式,尤其是利用z-index對元素層疊進行控制的時候,如果不理解層疊上下文的概念,就容易遇到各種各樣奇怪的問題,有時候,這些問題被錯誤的歸結為瀏覽器的“BUG”。實際上,大多數瀏覽器都是根據規范干活的,不要輕易地懷疑瀏覽器,而是要去看看規范中是怎樣定義規則的。
本文大量參考並引用MDN上的文字和源碼,並在其基礎上作些許改動以求更簡單明了的解釋。如果對源碼有疑問,請先去MDN上參考相關源碼和文獻。
本文是基於我對層疊上下文的學習和理解記錄而成,由於自己是初學者,不敢保證文中所有觀點都是正確的,因此我的觀點僅作參考,若發現文中有錯誤,歡迎大家指出,我會盡快作出修正。
