聊聊CSS中的層疊相關概念_CSS, z-index, BFC 教程
如果想要理解清楚CSS中的層疊相關的知識點,我們就很有必要先了解一些重要的概念:
- 文件流(Normal Flow)
- 格式化上下文(Formatting Context)
- 層疊上下文(Stacking Context)
- 層疊水平(Stacking Level)
- 層疊順序(Stacking Order)
文件流
在CSS中,文件流是一個很基礎也是很重要的一個概念。很多時候她被稱為Document Flow,但在CSS的標準被稱為Normal Flow,即普通流或常規流。大家更喜歡稱之為文件流。那麼CSS的文件流是怎麼一回事呢?
在HTML中任何一個元素其實就是一個物件,也是一個盒子。在預設情況下它是按照出現的先後順序來排列,而這個排列的順序就是文件流。
文件流是元素在Web頁面上的一種呈現方式。所有的HTML元素都是塊盒子(Block Boxes,塊級元素)或行內框(Inline Boxes,行內元素)。當瀏覽器開始渲染HTML文件時,它從視窗的頂端開始,經過整個文件內容的過程中,分配元素需要的空間。除非文件的尺寸被CSS規則限定,否則瀏覽器垂直擴充套件文件來容納全部的內容。每個新的塊級元素渲染為新行。行內元素則按照順序被水平渲染直到當前行遇到邊界,然後換到下一行垂直渲染。
事實上,在普通文件流中的盒子屬於一種格式化上下文(Formatting Context),大家較為熟悉的就是塊格式化上下文(Block formatting context)和
綜合上面的描述,也可以理解格式化上下文對元素盒子做了一定的範圍的限制,其實就是類似有一個width
和height
做了限制一樣。如果從這方面來理解的話,普通流就是這樣的一個過程:
- 在對應的塊格式化上下文中,塊級元素按照其在HTML原始碼中出現的順序,在其容器盒子裡從左上角開始,從上到下垂直地依次分配空間層疊(Stack),並且獨佔一行,邊界緊貼父盒子邊緣。兩相鄰元素間的距離由
margin
- 在對應的行內格式化上下文中,行內元素從容器的頂端開始,一個接一個地水平排列。
扯了這麼多,如果簡單的描述就是:如何排列HTML元素而已。拿個塊格式化上下文的普通文件流來舉例,就像下面這樣:
JavaScript12345 | <div>1</div><div>2</div><div>3</div><div>4</div><div>5</div> |
對應的效果如下:
上例中看到的文件流就是一個普通的文件流,也是一個正常的普通文件流。
在CSS中也可以通過float
或者position:absolute
兩種方法讓元素脫離文件流。而這兩者的表現實際上非常相似。簡單的可以理解為部分無視和完全無視的區別:
使用
float
脫離文件流時,其他盒子會無視這個元素,但其他盒子內的文字依然會為這個元素讓出位置,環繞在周圍(可以說是部分無視)。而對於使用position:absolute
脫離文件流的元素,其他盒子與其他盒子內的文字都會無視它(可以說是完全無視)。
格式化上下文
在介紹文件流的一節中,多次提到了格式化上下文這個概念。那麼格式化上下文指的又是什麼呢?
格式化上下文指的是初始元素定義的環境。
其主要包含兩個要點,一個是元素定義的環境,另一個是初始化。
在CSS中,元素定義的環境有兩種,也就是前面提到:塊格式化上下文和行內格式化上下文。這兩種上下文定義了在CSS中元素所處的環境,格式化則表明了在這個環境中,元素處理此環境中應當被初始化。用一句話來描述就是:
元素在此環境中應當如何排版佈局等。
塊格式化上下文其實也是大家常常稱為的BFC,指的是Web頁面的視覺化CSS渲染的一部分,是佈局過程中生成塊級盒子的區域,也是浮動元素與其他元素的互動限定區域。
下列方式會建立塊格式化上下文:
- 根元素或包含根元素的元素
- 浮動元素(元素的
float
不是none
) - 絕對定位元素(元素的
position
為absolute
或fixed
) - 行內塊元素(元素的
display
為inline-block
) - 表格單元格(元素的
display
為table-cell
,HTML表格單元格預設為該值) - 表格標題(元素的
display
為table-caption
,HTML表格標題預設為該值) - 匿名錶格單元格元素(元素的
display
為table
、table-row
、table-row-group
、table-header-group
、table-footer-group
(分別是HTMLtable
、row
、tbody
、thead
、tfoot
的預設屬性)或inline-table
) overflow
值不為visible
的塊元素display
值為flow-root
的元素contain
值為layout
、content
或strict
的元素- 彈性元素(
display
為flex
或inline-flex
元素的直接子元素) - 網格元素(
display
為grid
或inline-grid
元素的直接子元素) - 多列容器(元素的
column-count
或column-width
不為auto
,包括column-count
為1
) column-span
為all
的元素始終會建立一個新的BFC,即使該元素沒有包裹在一個多列容器中(標準變更,Chrome bug)。
建立了塊格式化上下文的元素中的所有內容都會被包含到該BFC中。
BFC是一個比較抽象的概念。如果要徹底的講述清楚,那麼可以用幾篇的篇幅來闡述,如果你想深糾的話,建議你花一些時間閱讀以下這些文章:
相對於塊格式化上下文,在行內格式化上下文中,盒子( Boxes )一個接一個地水平排列,起點是包含塊的頂部。 水平方向上的 margin
,border
和 padding
在盒子之間得到保留。 盒子在垂直方向上可以以不同的方式對齊:它們的頂部或底部對齊,或根據其中文字的基線對齊。 包含那些框的長方形區域,會形成一行,叫做行框。
在CSS中,對於行框這樣的東東涉及的頁就更多了。這裡不做過多的闡述。感興趣的同學,自己可以閱讀相關規範深究。
三維空間
平時我們從裝置終端看到的HTML文件都是一個平面的,事實上HTML文件中的元素卻是存在於三個維度中。除了大家熟悉的平面畫布中的x
軸和y
軸,還有控制第三維度的z
軸。
其中x
軸通常用來表示水平位置,y
軸來表示垂直位置,z
軸表示螢幕內外方向上的位置。
對於x
和y
軸我們很易於理解,一個向右,一個向下。但對於z
軸,理解起來就較為費力。在CSS中要確定沿著z
軸排列元素,表示的是使用者與螢幕的這條看不見的垂直線:
從正常流的一節中我們可以知道,如果元素不脫離文件流,或者不通過其他CSS的規則來改變初始化的格式化上下文環境,元素盒子是不可能會有層疊在一起的。但我們使用float
或position:absolute
時可以讓元素脫離文件流。那麼問題來了:
- 當一個設定了
z-index
值的定位元素與常規文件流中的元素相互重疊的時候,誰會被置於上方? - 當定位元素與浮動元素相互重疊的時候,誰會被置於上方?
- 當定位元素被巢狀在其他定位元素中時會發生什麼?
要回答這些問題,我們需要進一步地理解z-index
是如何工作的,尤其是層疊上下文,以及層疊次序這些概念。
層疊上下文
上一節提到過,網頁及其每個元素都有一個座標系統。該系統包括一個三維z
軸,其中的元素是層疊(Stacked)的。z
軸的方向指向檢視者,x
軸指向螢幕的右邊,y
軸指向螢幕的底部。
通常,瀏覽器會按照CSS規範中指定的特定順序放置元素:
在DOM樹中最先出現的元素被放在首位,之後出現的元素被放在前面的元素之上。但它並不總是那麼簡單。只有當頁面上的所有元素是自然流才起作用。也就是說,當沒有元素在流中的位置被改變或者已經脫離文件流,才起作用。
CSS中有兩種方式影響元素的流和位置的方法:
- 使用
position
屬性定位元素。除了預設的static
值外的元素被稱為定位元素 - 通過使用
float
屬性浮動元素來改變元素的流
事實上,每個HTML元素都屬於一個層疊上下文。給定層疊上下文中的每個定位元素都具有一個整數的層疊層級,具有更大堆疊級別的元素盒子總是在具有較低堆疊級別的盒子的前面(上面)。盒子可能具有負層疊級別。層疊上下文中具有相同堆疊級別的框根據文件樹出現的順序層疊在一起。
文件中的層疊上下文由滿足以下任意一個條件的元素形成:
- 根元素 (HTML)
z-index
值不為auto
的 絕對/相對定位position
值為fixed
或sticky
- 一個
z-index
值不為auto
的 Flex 專案 (Flex item),即:父元素display: flex|inline-flex
opacity
屬性值小於1
的元素transform
屬性值不為none
的元素mix-blend-mode
屬性值不為normal
的元素filter
、perspective
、clip-path
、mask
、mask-image
、mask-border
、motion-path
值不為none
的元素perspective
值不為none
的元素isolation
屬性被設定為isolate
的元素- 在
will-change
中指定了任意 CSS 屬性,即便你沒有直接指定這些屬性的值 -webkit-overflow-scrolling
屬性被設定touch
的元素
而且每個網頁都有一個預設的層疊上下文。這個層疊上下文的根源就是html
元素。html
元素中的一切都被置於這個預設的層疊上下文的一個層疊層上。理解起來有點怪。那麼先來看一個圖:
層疊上下文1 (Stacking Context 1)是由文件根元素形成的。 層疊上下文2和3 (Stacking Context 2, 3) 都是層疊上下文1 (Stacking Context 1) 上的層疊層。 他們各自也都形成了新的層疊上下文,其中包含著新的層疊層。
這樣是不是有點理解,如果沒有,也不要緊,先把這個概念放下,先來理解另外兩個概念:層疊水平和層疊順序。
疊層水平
@張鑫旭 老溼在《深入理解CSS中的層疊上下文和層疊順序》一文中把疊層水平描述的非常表象又易於理解。我就直接做為搬運工,把這部分內容搬過來佔為己用了。
層疊水平(Stacking Level)決定了同一個層疊上下文中元素在z
軸上的顯示順序。Level
這個詞很容易讓我們聯想到我們真正世界中的三六九等、論資排輩。在真實世界中,每個人都是獨立的個體,包括雙胞胎,有差異就有區分。例如,又胞胎雖然長得很像,但實際上,出生的時間還是有先後順序的,先出生的那個就大(大哥或大姐)。網頁中的元素也是如此,頁面中的每個元素都是獨立的個體,他們一定是會有一個類似排名排序的情況存在。而這個排名排序、論資排輩就是我們這裡所說的層疊水平。層疊上下文元素的層疊水平可以理解為官員的職級,一品兩品,縣長省長之類;對於普通元素,這個嘛…你自己隨意理解。
於是,顯而易見,所有的元素都有層疊水平,包括層疊上下文元素,層疊上下文元素的層疊水平可以理解為官員的職級,一品兩品,縣長省長之類。然後,對於普通元素的層疊水平,我們的探討僅僅侷限在當前層疊上下文元素中。為什麼呢?因為否則沒有意義。
這麼理解吧~ 上面提過元素具有層疊上下文好比當官,大家都知道的,這當官的家裡都有丫鬟啊保鏢啊管家啊什麼的。所謂打狗看主人,A官員家裡的管家和B官員家裡的管家做PK實際上是沒有意義的,因為他們牛不牛逼完全由他們的主子決定的。一人得道雞犬升天,你說這和珅家裡的管家和七俠鎮婁知縣縣令家裡的管家有可比性嗎?李總理的祕書是不是分分鐘滅了你村支部書記的祕書(如果有)。
翻譯成術語就是:
普通元素的層疊水平優先由層疊上下文決定,因此,層疊水平的比較只有在當前層疊上下文元素中才有意義。
需要注意的是,諸位千萬不要把層疊水平和CSS的z-index
屬性混為一談。沒錯,某些情況下z-index
確實可以影響層疊水平,但是,只限於定位元素以及Flex盒子的孩子元素;而層疊水平所有的元素都存在。
層疊順序
在HTML文件中,預設情況之下有一個自然層疊順序(Natural Stacing Order),即元素在z
軸上的順序。它是由許多因素決定的。比如下面這個列表,它顯示了元素盒子放入層疊順序上下文的順序,從層疊的底部開始,共有七種層疊等級:
- 背景和邊框:形成層疊上下文的元素的背景和邊框。 層疊上下文中的最低等級。
- 負
z-index
值:層疊上下文內有著負z-index值的子元素。 - 塊級盒:文件流中非行內非定位子元素。
- 浮動盒:非定位浮動元素。
- 行內盒:文件流中行內級別非定位子元素。
z-index: 0
:定位元素。 這些元素形成了新的層疊上下文。- 正
z-index
值:定位元素。 層疊上下文中的最高等級。
這七個層疊等級構成了層疊次序的規則。 在層疊等級七上的元素會比在等級一至六上的元素顯示地更上方(更靠近觀察者)。 可以結合w3help中的一張圖來幫助我們更好的理解這七個層疊等級:
其實對於層疊順序規則還是較為複雜的。
當頁面包含浮動元素、絕對定位的元素、固定定位的元素或相對定位的元素(元素從正常位置偏移一定量)以及內聯元素時,瀏覽器會以不同的方式顯示它們(放置它們)。元素從最靠近檢視者的地方排列到最遠的地方,如下所示:
- 定位元素按原始碼中的外觀順序排列。原始碼中的最新內容最接近檢視者
- 內聯元素(比如文字和影象)是流入和非定位(它們的位置是靜態的)
- 非浮動元素按照原始碼中外觀的順序排列
- 非定位和非浮動塊級元素
- 根元素
html
是全域性層疊上下文的根,包含頁面上的所有元素
這就是瀏覽器在呈現頁面上的元素時應用的預設層疊順序。
如果你想要更改定位元素在z
軸上的渲染順序,可以使用z-index
屬性。例如,你有兩個絕對定位的元素,它們在某個點上重疊,並且你希望其中一個元素顯示在另一個元素的前面,即使它在原始碼中出現在它之前,你也可以使用z-index
屬性來實現這一點。
此時需要注意的第一件重要的事情是,z-index
屬性只適用於定位元素。所以,即使為元素提供z-index
的值將其置於其他元素之前,z-index
也不會對元素產生影響,除非它被定位;也就是說,除非它具有除static
之外的position
值。
因此,如果所有定位的元素具有z-index
的索引值,則將元素從最靠近檢視者排列到最遠的位置,如下所示:
- 具有正值的
z-index
的定位元素。較高的值更接近螢幕。然後,按照它們出現在原始碼中的順序排列 - 定位元素的
z-index:0
或z-index: auto;
- 內聯元素(如文字和影象)是流中的和非定位的(它們的位置是靜態的)
- 原始碼中出現順序的非定位浮動元素
- 非定位和非浮動塊級元素
- 具有負值的
z-index
的定位元素。較低的z-index
索引值更近。然後按照它們在原始碼中出現的順序 - 根元素
html
是全域性層疊上下文的根,包含頁面上的所有元素
當我們在定位元素上設定z-index
值時,它指定該元素在它所屬的層疊順序上下文中的順序,並且它將根據上述步驟在螢幕上渲染。
但是,當我們設定元素的z-index
時會發生另一件事。獲取除預設值auto
之外的z-index
值的元素實際上為其所有定位的後代元素建立層疊上下文。我們之前提到過,每個層疊上下文都有一個根元素,它包含其中的所有元素。當你將z-index
屬性應用於這個元素時,它將在其包含的下下文中指定元素的z
軸順序,並且還將建立以該元素為根的新層疊順序上下文。
一個具有值為
z-index:auto
的定位元素被視為建立了新的堆疊順序上下文,但任何實際建立新層疊順序上下文的定位後代和後代被視為父層疊順序上下文的一部分,而不是新的層疊順序上下文。
當一個元素成為一個新的層疊順序上下文時,它所定位的後代元素將會按照我們前面提到的元素本身的規則在其中進行層疊渲染。因此,如果我們再次重寫渲染過程,它會是這樣的:
- 具有正值
z-index
的定位元素組成的層疊順序上下文。較高的值更接近螢幕。然後按照它們在原始碼中出現的順序呈現 - 定位元素的
z-index: 0
或z-index: auto
- 內聯元素(比如文字和影象)是流中的和非定位的(它們的位置是靜態的)
- 非浮動元素按照原始碼中外觀的順序排列
- 非定位和非浮動塊級元素
- 具有負值
z-index
的定位元素組成的層疊順序上下文。較低的z-index
的值更接近螢幕。然後按照它們在原始碼中出現的順序呈現 - 根元素
html
是全域性層疊上下文的根,包含頁面上所有元素
因此,當我們使用z-index
屬性來確定其層疊順序中定位元素的順序時,我們還建立了“原子(Atomic)”層疊順序上下文,其中每個元素成為其所有定位後代的層疊順序上下文。
視覺化理解層疊上下文
前面涉及的都是一些概念和理論。或許很多同學覺得這些概念很枯燥,難於理解。其實我們可以把這些概念結合到實際生活中來理解。比如@codrops在《CSS Reference: z-index
》文中舉的漢堡相關的示例。
你可以將構成層疊的元素視為你小時候可能玩過的積木。這些積木是一堆不同顏色的圓形木塊,你可以把它們堆在一起。
一個木製的堆疊塔。彩色的圓形積木類似於層疊環境中的元素。木塔(方形的基座)是包含彩色圓形積木的層疊環境。
現在,想象一下兩座塔緊挨在一起,每個塔上都有一堆圓圈,彼此緊挨著。這兩個塔類似於頁面上的兩個定位元素,每個元素都為其後代形成一個層疊上下文。
每個堆疊塔代表一堆積木的堆疊環境。
當兩個層疊上下文重疊時,它會變得更復雜(但並不困難)。為了理解層疊上下文重疊時會發生什麼,可以想想一個漢堡包三明治。
每個漢堡包都含有堆疊在一起的食物(乳酪、西紅柿、洋蔥,如果你不是素食者的話,可能是肉)。每個漢堡包代表其內部食物切片的堆疊環境。在第二個旁邊的另一個漢堡包也是其內部切片的層疊上下文。現在,想象一下把兩個漢堡放在一起。頂部的兩個漢堡代表頁面上重疊的兩個定位元素。通過將兩個漢堡包堆疊在一起,你實際上已經給了一個比底部更高的層疊順序。
你可以想象,上漢堡裡的食物片不能比上漢堡裡的食物片高 —— 它們被限制在它們的疊加環境中,並且無論它們的z-index
指數有多高,都將保持在它的邊界內。
下圖展示了一個生活中真實的層疊上下文的例子。它由幾個重疊的層組成。每一層都是堆放書和其他東西的上下文。從底層第二層的書總是放在上層內容下面。除非對層進行更改或重新定位(經定不同的z
軸的值),否則總是如此。
層疊上下文A的內容可以放在另一個層疊上下文B的內容前面的唯一方法是,給A一個大於B的z-index
值,當然,它們的前提是有一個相同層疊上下文的環境。
我們要講的最後一個視覺化示例可能描述元素在頁面上繪製方式的最好例子之一。
網頁實際上就像一幅油畫畫布。瀏覽器按照一定的順序繪製畫布上的元素,就像畫家在畫布上繪製物件一樣。從最遠處開始,再到最近的。下面的繪製先用Mr.Z
來描述:
如果你根據“畫家演算法”來思考,即物體被畫在一個場景上的前後順序,那麼層疊上下文就像一幅畫中的一幅畫。首先,你要按照正確的順序把所有的東西都畫在後面,然後當你要畫它的父上下文時,把整個結果貼在它所屬的地方。
層疊演算法在每個原子層疊上下文中都與在全域性根(html
)上下文中一樣。
構成層疊上下文的元素的背景和邊框總是落在上下文中的所有元素後面。
我們到目前為止所提到的要點可以從CSS規範中歸納為以下幾點:
- 渲染樹在畫布上繪製的順序是按照層疊上下文來描述的。層疊上下文可以包含進一步的層疊上下文。從父層疊上下文的角度來看,層疊上下文是原子的;其他層疊上下文中的邊框可能不會位置任何邊框之間
- 每個盒子屬於一個層疊上下文。在給定的層疊上下文中,每個定位盒子都有一個整數的層疊級別,它在
z
軸上的位置相對於同一層疊上下文中的其他層疊級別。具有較高層疊級別的盒子總是在具有較低層疊級別的盒子前面格式化。盒子可能有負的層疊級別。在層疊上下文中,具有相同層疊級別的盒子按照文件樹順序從後到前堆疊 - 根元素形成了層疊上下文。其他層疊上下文是由任何定位元素(包括相對定位的元素)生成的,其計算值為
z-index
,而不是auto
。層疊上下文不一定與包含塊相關。
總結
對於眾多CSSer來說,閱讀CSS的規範和理解相關的概念都是枯燥無味的。而且很多同學理解一些概念都比較吃力。比如這篇文章中提到的相關概念: 文件流(Normal Flow)、格式化上下文(Formatting Context)、層疊上下文(Stacking Context)、層疊水平(Stacking Level) 和 層疊順序(Stacking Order)。
雖然這些概念是CSS的基礎,但很多同學都一直不願去觸碰,因為它們看起來簡單,事實上還是較為複雜的。如查我們花一定的時間理解了這些概念,能幫助我們更好的理解CSS中其他相關的概念和知識點,特別是z-index
的運用。
文章過於理論化,有理解不透徹或不對之處,歡迎大神拍正。