CSS Display & Position W3C 規格閱讀筆記

22 min read

# front-end

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

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

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

the inner display type, which defines (if it is a non-replaced element) the kind of formatting context it generates, dictating how its descendant boxes are laid out. (The inner display of a replaced element is outside the scope of CSS.)

the outer display type, which dictates how the principal box itself participates in flow layout.

口語一點來說,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, boxes are laid out one after the other, vertically, beginning at the top of a containing block. The vertical distance between two sibling boxes is determined by the 'margin' properties. Vertical margins between adjacent block-level boxes in a block formatting context collapse.

In an inline formatting context, boxes are laid out horizontally, one after the other, beginning at the top of a containing block. Horizontal margins, borders, and padding are respected between these boxes. The boxes may be aligned vertically in different ways: their bottoms or tops may be aligned, or the baselines of text within them may be aligned. The rectangular area that contains the boxes that form a line is called a line box.

有點麻煩就不翻譯了,簡言之,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 排列組合後,有一種結果滿出乎我自己預料:

row display: inline / col display: block

以上原先預想的是,在 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 敘述中特別看到:

This module replaces and extends the positioning scheme features defined in [CSS2] sections。

基本上 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 的資料:

Most of the time, absolutely positioned elements that have height and width set to auto are sized so as to fit their contents. However, non-replaced, absolutely positioned elements can be made to fill the available vertical space by specifying both top and bottom and leaving height unspecified (that is, auto). They can likewise be made to fill the available horizontal space by specifying both left and right and leaving width as auto.

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

The box's position (and possibly size) is specified with the 'left', 'right', 'top', and 'bottom' properties. These properties specify offsets with respect to the box's containing block ...

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 上的一則解釋

In general, the content-based minimum size of a flex item is the smaller of its content size suggestion and its specified size suggestion. However, if the box has an aspect ratio and no specified size, its content-based minimum size is the smaller of its content size suggestion and its transferred size suggestion. If the box has neither a specified size suggestion nor an aspect ratio, its content-based minimum size is the content size suggestion.

簡言之,當 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: 初始寬高。

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

小明跟小華一起共同創業拿出了自己 100 塊給 flex 股份有限公司,且因為小華有特別的技術,所以約定好了小明擁有 4 成股份跟小華擁有 6 成股份

  • 一年之後 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 來限制。

參考資料

Footnotes

  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 計算,詳細可以參考 這篇文章