min.

深入 CSS Display & Position

2020-01-15# front-end# css

CSS 大概是前端工程師的切版日常風景之一,雖然現在已經越來越多成熟的 CSS 預處理、或 Design System 打包好 Component Library,但 CSS 還是入門前端的時候的必經之路。

但每每提到切版的時候,都會有一種切版像是在鑽 CSS 的漏洞,各種 CSS tricks 跟 hack 讓人防不勝防,也讓不少前端工程師產生 CSS 可能是最簡單也最困難、像是蛋炒飯一樣的語言。

於是想要來深入研究一下這個每天都會使用到的 CSS 排版基礎屬性:Display 跟 Position 的原理到底是什麼?

CSS 排版

在管理 CSS 排版方式主要仰賴兩大屬性 Position 跟 Display,在過去使用不深究這兩個屬性的時候,大抵會知道的就是:如果 Position 不特別做什麼的時候,元素會根據自己的寬度由左上到右下的排列、接著元素要怎麼排?要都一列列?還是都在同一列?抑或是像表格一樣?則會再用 Display 去控制他,由此甚至衍伸出 N 種因應不同狀況的水平垂直置中手法。

此時簡易地用一條河來比喻,元素就像是飄在水上的小船,Position 就是小船在水流中飄的方式,要在順流而行?還是獨立放錨,在河中的某個位置? Display 就像是小船之間應該怎麼排列跟小船上貨物要怎麼放的規則。

在 CSS1 的時候還沒有 Position 屬性,只有定義幾種 Display 方式,例如:block, inline, list-item 跟 none,但雖然沒有這個屬性,不意味著就是沒有 Position 的概念,當時在 float 屬性的描述裡面,就有提到 Normal Flow 排版流。只是直到 CSS2 的時候 CSS 才針對不同的 Position 訂出了 Flow Layout,並增添了新的 Display 屬性,才有現在 Position 與 Display 的排版雛形。

CSS Display

CSS 使用上最讓人害怕的就是用著用著突然發現預期的效果沒有出現,例如:有的時候加了 margin 但元素卻沒有移動、使用了 vertical align 卻沒有 align 諸如此類,往往發生這些狀況的時候,仔細 debug 之後就會發現:元素容器與元素的關係常常是出問題的關鍵,但為什麼?為什麼不是這個元素是什麼 Display 就怎麼排嗎?

看到 CSS3 針對 Display 屬性的介紹,可以發現 Display 其實裡面暗藏了兩個屬性:

口語一點來說,the inner display type 就像是力場一樣,丟進去的物體會被這個力場影響排列方式,the outer display type 則是這個力場本身的形狀。以 flex 來說:flex 的 display 全名應該是 block flex,意思就是元素內以 flex formatting context 的規則來進行排列,外部則是以 block 的方式來呈現,如下圖所示:

所以我們常常使用的 flex, block 等等都只是縮寫,全名應該參考 W3C CSS Full Display 的定義:

從表中我們可以看到一些常見的 inner display type 跟常見的 outer display type。接下來會就一些容易混淆的基礎屬性來了解他的定義,由於定義方式脫離平常使用的理解非常遙遠,可以先略過 outer display type 與 inner display type 這兩個段落。

outer display type

outer display type 常見的屬性主要是兩種,這是 CSS2 就定義好的屬性:

  • block: 會創建 Block-Level Box 置於 Normal Flow 之中。
  • inline: 會創建 Inline-Level Box 置於 Normal Flow 之中。

那什麼又是 Block-Level Box, Inline-Level Box 呢?回溯至 CSS2 可以再粗略分成四種狀況(不考慮 float 的情況下)可以直接跳到最後的簡要:

  1. Inline-Level Box Non-Replaced Element 1
  • width 不能被設定
  • margin-left, right 在 auto(未特別設定) 時為 0
  1. Inline-Level Box Replaced Element
  • margin-left, right 在 auto 時為 0
  • height, width 在 auto 時,引入元素的 width。
  • height, width 在 auto 時,引入元素也沒有特別的 width,但卻有 height 則 width = (used height) \* (intrinsic ratio)
  • height, width 在 auto 時,引入元素也沒有特別的 width 與 height,建議當作 block-level box non-replaced elements 處理。
  1. Block-Level Box Non-Replaced Element
  • auto 的寬度空間預設為:母元素並由 margin + padding + width 而成。
  • 如果 width 不為 auto 且上面相加起來大於母元素,那 margin 當 0。
  • 如果如果有任一值為 auto,對應值則相等。
  • 如果 width 設定 auto,其他屬性為 0。
  • 如果 margin-left 與 margin-right 為 auto, 則讓這兩個得到相等的值,造成水平置中。
  1. Block-Level Box Replaced Element
  • width 同 Inline-Level Box Replaced Element。

spec 真不愧是天書,如果沒有操作過真的不知道上面寫的是什麼意思,所以實務來看:

  • inline 是根據子元素,不能設定 width 的屬性,並出現在同一行的元素。
  • block 則是先撐滿母元素後,再來根據設定計算 width, margin, padding 屬性的元素。

順帶一提,在了解 block 的過程中,看到重新認識 CSS 系列將非常複雜的 block container, block-level box, block 等名詞做了很好的分析,一開始看 W3C Spec 的時候真的覺得非常混淆。

inner display type

inner display type 常見的則是有下面幾種,最容易混淆的屬上面兩種:

  • flow: 如果 outer 是 inline 的話,且母元素的 inner 為 block 或 inline formatting context 2 的話,則為創建 inline formatting context,否則為 block level formatting context。
  • flow-root: 在內部創建 block container box 空間, 並讓內側元素自然跟隨 flow layout。
  • table, flex, grid ... 三種不特別介紹,因為它們都有一套自己獨特的規則,相對不容易混淆。

但上面兩種以上應該從屬性倒回來看比較清楚:

  • inline-block (inline | flow-root) = inline-level box + block formatting context。
  • block (block | flow) = block-level box + block formatting context。
  • inline (inline | flow) = inline-level box + inline formatting context。

那到底 block formattign context 與 inline formatting context 是什麼呢?又要跳回 CSS2 來了解:

有點麻煩就不翻譯了,簡言之,block formatting context 是指所有東西都會垂直比鄰而居的情境,inline formatting context 則是東西會水平比鄰而居。

名詞比對

研究到這邊告一段落,這邊來個懶人包,拉出不同名詞再來回顧我們的 display 到底在做什麼:

  • Display 這個屬性,由兩個值來決定分別是 inner display type 與 outer display type。
  • inner display type 根據不同類別創建不同 formatting context
  • outer display type 根據不同類別創建不同 level box
  • level box 會決定元素與外面元素的關係,包含:自己的 width、height、margin。
  • formatting context 會決定元素內的排列規則,包含:內部元素的排列方向、順序、對齊方式。

Display Type

研究到這裏會很直覺地想到,這樣元素的 outer display type 跟 inner display type 會不會互相干擾?屬性描述來看並沒有 inner 跟 outer 之間會互相矛盾的地方,所以以下就 2*5 種外層元素與內層元素的排列組合來了解樣式的變化:

在嘗試不同的 inner type display 與 outer type display 排列組合後,有一種結果滿出乎我自己預料:

以上原先預想的是,在 inline 的 row 裡面 col 垂直比鄰而居,但這種情境必須在 row display: inline-block 的情況下才會發生。現實是,col outer 為 block-level box 的時候,row 為 inline formatting context 會崩潰,仔細想想也可以理解,硬要撐滿的 block-level box 在一定要水平比鄰的 inline formatting context,結果就是會爆掉。

這也就是為什麼後來才有 inline-block 這個值的原因了。

CSS Position

了解完了比較複雜的 Display 可以回頭再來看看 Position 是什麼,在 CSS3 裡面 Position 會創建四種的 Positioning Schemes,Position Schemes 這個名詞跟 Formatting Context 一樣,平常我們使用的 Static, Relative 是 Position 的值,這些值會創建下面四種 Schemes:

  • Relative positioning
  • Sticky positioning
  • Absolute positioning
  • Fixed positioning

有趣的是,可以在 CSS3 敘述中特別看到:

基本上 CSS3 許多屬性已經在很多瀏覽器實作出來且指在取代 CSS2 原本三種 Positioning: Normal Flow, Float, Absolute Positioning 概念,但 CSS2 的三種概念依舊存在,所以跟 Display 相比,Position 的複雜在於不同版本的 Positioning Module。原則上不推薦將 CSS2, 3 兩邊的 Positioning 交互使用,即便 CSS3 在 CSS Positioned Layout Module 第六章有進行兩者之間的比較。

CSS3 Position & Display

為求單純,先針對 CSS3 Positioning 與其中的四種 Positioning Schemes 來討論。還記得前面的河流比喻嗎?在 CSS 排版中,預設的 Normal Flow 就像一條河流由左上到右下的排列下去,延續比喻,四個 Schemes 就像是這樣:

  • Relative positioning:受河流影響,可以依著河流相對位移。
  • Sticky positioning:受河流影響,直到撞到一個障礙物卡住。
  • Absolute positioning:跳脫河流的影響,直接下錨決定要在相對於河岸的特定位置。
  • Fixed positioning:跳脫河流的影響,直接決定要在整個區域的特定位置。

一樣我們實際玩玩看,讓 outer display type Inline 與 Block 觀察在不同的 Position 下有什麼不同,這邊是實驗的 code sandbox

可以發現,在 Position Absolute 跟 Fixed 「看似」會讓 outer display type 直接變成 inline。這一點,看了一些資料,根據 Morzilla Developer position 的資料:

發現其實不是變成了 inline-level block,而是他會被設定的 top, left, right, bottom 壓縮 width 跟 height,直到容器內原本所佔的寬高。這部分,可以跳回 CSS2 absolute schemes 跟 block-level box 的定義來了解:

Absoltute 會讓 Box 與可能的尺寸,被 top, left, right, bottom 影響。

Flex

先回到 CSS3 Display Module,來看看到底什麼是 { display: flex },flex 其實是 outer display type block, inner display type flex 的一個屬性,雖然很少使用,但其實還有 { display: inline-flex }

Flex 出現的時代背景是在智慧型手機誕生的年代,越來越複雜的 UI 讓 W3C 提出新的 Module 來優化 UI 排版,一開始是針對一維的排版而設計:

但是由於 flex 實在是太太太 ...... 好用了,所以在 Grid 屬性出來之前,就算是全頁排列,也都會用 Flex 來湊。

而時至今日,Flex 已經算是相當普及的 CSS 屬性,但以防萬一如果需要支援 IE 的狀況,Flex 在 IE 還是有部分不支援的狀況喔:

Flex Container

回到 Flex 屬性,Flex 會讓套用 flex 的元素帶有 Flex Container 的特性,其中子元素則稱之為 Flex Item,從官方下圖可以很清楚的看出在 Flex 狀態下的元素關係:

在 Flex Container 層級,主要決定了子元素的排列、對齊方式,這滿符合對 Formatting Context 的想像,常見的屬性如下:

  • flex-direction: 決定了 main axis 跟 cross axis 的位置與方向,單從視覺上來看,決定了內部元素的垂直、水平、正反排列方式。
  • justify-content:對齊 main axis 的方式,如果在 row 的情況下,就會像是水平對齊,反之在 column 情況下看起來像是垂直對齊。
  • align-items:對齊 cross axis 的方式,如果在 row 的情況下,就會像是垂直對齊,反之在 column 情況下看起來像是水平對齊。
  • align-contents: 多行物件對齊 cross axis 的方式,同樣的也會受到 axis 軸向變化的影響。

為什麼要繞這麼大圈圈,原因其實就跟上面 direction 的實作方式有關,因為 flex-direction 的改變其實是改變了整個座標軸而不是改變元素,所以這兩個屬性代表的對齊效果才會跟著改變:

  • flex-wrap:flex container 內的元素要不要換行,不換行的情況下,如果 flex item 超過 flex container 的寬度,則會壓縮 flex item 的寬度。

Automatic Minimum Size

結果在 nowrap 且 content 長度突破天際的時候,flex 會爆炸,但事實上壓縮的時候是符合自己的預期,為什麼呢?因為 flex 已經沒有空間壓縮 flex-item 了,在 flex item 固定為 block 的情況下,flex item 的內容預設會換行直到「不能再換行」。

我們可以回到 CSS3 看 Automatic Minimum Size of Flex Items 的定義跟 stack overflow 上的一則解釋

簡言之,當 flex item 被壓縮到:

  • 在 nowrap 保持同一行的狀況下
  • 又沒有任何 padding 可以壓縮
  • 又小於 flex item 預設的 min-width 3

則會超出 container,讓 flex item 展開,很有趣的是在測試什麼叫最小的 min-width 的時候,發現只是一長串文字,例如:'LONGTEXTLONGTEXTLONGTEXTLONGTEXT' 跟假文製造機的相比,破版的狀況不同,因為英文的最小寬度是一個單字、當不透過空白分開的時候,他會當作一個長單字,類似的狀況也可以在中文看到,中文的 min-width 是一個字,大家可以試玩看看~

Flex Item

接著進入到 Flex Item,除了上述提到的 Flex Direction 可以決定 Flex Item 的順序之外,我們也可以在 Flex Item 上透過 Order 來設定單一元素的順序。除此之外,Flex Item 最強大惡屬性設定是,可以設定子元素寬度的縮放比例:

  • flex-grow: 以多少比例得到多的空間。
  • flex-shrink: 以多少比例被剝奪少的空間。
  • flex-basis: 初始寬高。

概念上就跟投資比例很類似思考,舉一個小故事為例(跟現實中的分成原則略有不同,僅供示意):

  • 一年之後 flex 股份有限公司幸運的賺到了 100 塊,所以公司總資產 300 塊,今天如果賣掉公司,小明可以拿回自己的 100 塊 + 賺到的 100 * 0.4 也就是 140,而小華則是 160。
  • 一年之後 flex 股份有限公司倒賠了 100 塊,所以公司總資產 100 塊,今天如果賣掉公司,小明的 100 塊會少掉倒貼的 100 * 0.4 最後剩下 60,小華則是剩下 40。

一開始兩人投資的金錢就是 flex-basis,上面就是 flex-grow 下面就是 flex-shrink 的比例,類比到 width 之中:

所以這也是為什麼預設的 Flex Shrink 為 1,Flex Grow 為 0,當空間不夠的時候以自己持有的 1:1 的方式壓縮,當空間足夠的時候則先不填滿 4

Flex Item RWD

從上面我們可以了解到怎麼用 Flex 屬性做出非常簡單的 RWD,那當 Flex 遇到平常會使用的另外一組 RWD 屬性 min-width 與 max-width 會發生什麼事呢?

在空間充足的情況下,預設寬度會以 flex-basis 優先,並且是 min, max-width 會最高順位的限制 flex grow & shrink 的成長。

常見的搭配可以用在,例如:Navigation 的區塊在大螢幕下不希望過度分佈的話,就可以在正常 flex grow, shrink 與 max-width 來限制。

參考資料


  1. Resplaced Element,即 CSS 無法先知道他的長寬,例如:HTML, IMG, INPUT, TEXTAREA, SELECT, OBJECT,因為這些元素的長寬往往會被取代(replaced),像是: img 會被代入的 src 影響的元素。
  2. 所謂的 formatting context 就是前面提及的力場,猜測之所以會再獨立出這個名詞,是因為屬性與規則之間不是一對一,例如:flow 就有 block formatting context 與 inline formatting context 這兩種情況。
  3. min-width auto 的計算方式根據 CSS3 spec 的定義,若有被上層 layout 特別定義,不然當作 0 看待。
  4. Flex 數字是可以小於 1 的,如果小於 1 則直接將小數點當作百分比來計算,無論 item 數值相加起來有沒有等於 1 都當作 1 計算,詳細可以參考 這篇文章
# front-end# css

© 2020 minw Powered by Gatsby