CSS Grid 深入淺出

CSS Flexbox 深入淺出

上一篇的 Flexbox 徹底改變了我們在網頁上排版的方式,但它其實只是整個佈局系統 (Layout) 的一部分。它還有個大哥就是所謂的網格佈局模組 (Grid Layout) 。這兩個佈局系統加起來,提供了一套非常完整又強大的佈局工具。

CSS 網格允許你定義出列與行,做出「二維佈局」,然後再把項目放進這個網格裡。有些項目可能只佔一個格子,或是跨好幾列或好幾行,我們可以很精確地設定每個格子的大小,也可以讓它們根據內容自動調整。項目可以精確地放在某個格子裡,也可以讓它們自己流動去填補空位。透過網格,我們可以打造出非常複雜的版面設計,就像下圖。

CSS Grid Layout

基礎:Basic Grid

我們先從一個很基礎的範例開始:把六個框排成三列。如下圖:

CSS Grid Layout

1
2
3
4
5
6
7
8
<div class="grid">    
<div class="a">a</div>
<div class="b">b</div>
<div class="c">c</div>
<div class="d">d</div>
<div class="e">e</div>
<div class="f">f</div>
</div>

就像 Flexbox 一樣,Grid 也是作用在 DOM 的父子層級。當你把某個元素設為 display: grid,它就變成了「網格容器」 (Grid Container) ,而它底下的子元素就會變成「網格項目」 (Grid Items)

1
2
3
4
5
6
7
8
9
10
11
12
.grid {
display: grid; /* 使元素變成網格容器 */
grid-template-columns: 1fr 1fr 1fr; /* 定義三個等寬的列 */
grid-template-rows: 1fr 1fr; /* 定義兩個等高的行 */
gap: 0.5em; /* 設定間距 */
}
.grid > * {
background-color: darkgray;
color: white;
padding: 2em;
border-radius: 0.5em;
}

我們用了 display: grid 把元素變成了「網格容器」。這個容器的行為就像一般的 block 區塊元素一樣,會自動填滿整個可用寬度。

雖然這裡沒寫到,不過你也可以用 inline-grid,這會讓元素像 inline 行內元素那樣跟文字一起流動,而且寬度只會剛好包住它的子元素。不過實際上,大多數情況還是會用 display: grid,用 inline-grid 的機會比較少。

接下來是兩個新的屬性:grid-template-columnsgrid-template-rows,用來定義每一列和每一行的大小。

這裡我們用了一個新的單位 fr,它的意思是「比例單位」,有點像 Flexbox 裡的 flex-grow。例如,grid-template-columns: 1fr 1fr 1fr 就是建立三個等寬的欄位,每個欄都佔一等份。不一定每一列或每一行都得用 fr,也可以用其他單位像是 pxem%,甚至可以混著用。像這樣:grid-template-columns: 300px 1fr,意思是第一欄寬度固定 300px,第二欄則佔據剩下所有可用空間。如果你寫 2fr,那它的寬度就是 1fr 的兩倍。

還有一個很實用的屬性是 gap,它是用來設定網格之間的間距,就跟 Flexbox 裡的 gap 一樣用法。你也可以一次設定兩個值,像是 gap: 0.5em 1em,這樣就能分別指定垂直和水平的間距。

在剛開始定案網格規範的時候,gap 這個屬性原本叫做 grid-gap,所以可能會在一些早期的範例中看到這個名稱。其實它的功能跟現在的 gap 是一樣的。後來 CSS 規範更新了,改為比較通用的 gap,而且連 Flexbox 也一起加入支援,變得更一致好用。

剖析:Grid

想要真正掌握網格系統,就要先搞懂它的各個術語。接下來,在網格系統裡,還有四個術語是一定要知道的:

  • 網格線 (Grid Lines) :網格線構成整個網格的結構,可以是垂直的或水平的,分別出現在列或行的兩邊。如果你有設 gap 的話,網格線就會在間距的兩側,或是說間距會出現在網格線的之間。
  • 網格軌道 (Grid Tracks) :是指兩條相鄰網格線之間的區域。軌道可以是橫向的 (行) 或直向的 (列) 。
  • 網格單元格 (Grid Cells) :網格裡的每一格,簡單來說,就是橫向和直向的軌道交錯出來的空間。
  • 網格區域 (Grid Areas) :由一個或多個單元格組成的長方形區塊,範圍是由兩條垂直網格線和兩條水平網格線包起來的。

CSS Grid Layout

建立網格佈局的過程中,會不斷參考網格裡的這些基本結構。舉個例子來說,當你寫下 grid-template-columns: 1fr 1fr 1fr,其實就是定義了三個等寬的垂直「網格軌道」。這樣一來,也就產生了四條垂直的「網格線」:一條在網格線的最左邊、兩條在每個欄位之間,還有一條在最右邊。

Note:你的設計不一定要把網格的每個格子都填滿!如果你有區域想要留空白,只要讓那個格子保持空著就可以。記得要放進網格的每個元素,都必須是網格容器的直接子元素。

另外我們介紹一個新的語法:repeat() 函數。這是一種簡寫方式,可以幫你快速建立多個重複的網格軌道。像這樣寫:grid-template-rows: repeat(4, auto),就等於是你寫了 auto auto auto auto,意思是總共四行,並且每個軌道高度會依照內容自動調整大小。

你也可以用 repeat() 來定義一種重複模式。舉例來說,repeat(3, 2fr 1fr) 會重複 2fr 1fr 這個組合三次,結果就會是六個欄位:2fr 1fr 2fr 1fr 2fr 1fr

CSS Grid Repeat

我們可以將先前的例子改寫成這樣:

1
2
3
4
5
6
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* repeat function */
grid-template-rows: repeat(2, 1fr);
gap: 0.5em;
}

定位:用數字編號定位網格線 Numbering grid lines

定義網格軌道後,我們將每個網格項目放置到特定位置上。瀏覽器為網格中的每條網格線指派編號,如下圖。 CSS 使用這些數字來指派每個項目應放置的位置

CSS Grid Repeat

用「網格編號」來指定每個項目要放在網格的哪個位置,這時候會用到 grid-columngrid-row 這兩個屬性。

比如說,如果你想讓某個項目跨越從第 1 條到第 3 條的垂直網格線,那就可以寫成:grid-column: 1 / 3。這樣它就會橫跨兩個欄位。同樣的道理,如果你想讓一個項目從第 3 條到第 5 條的水平網格線之間延伸,就可以寫成:grid-row: 3 / 5

這兩個屬性加起來,就能準確地指定一個項目要佔據的網格區域。

1
2
3
4
.card {
grid-column: 1 / 3;
grid-row: 3 / 5;
}

grid-columngrid-row 其實都是簡寫屬性。像 grid-column 其實是 grid-column-startgrid-column-end 的合併寫法,grid-row 也是一樣。只有在用簡寫的時候,才需要用斜線 / 來分隔起始和結束的位置。而那個斜線前後要不要加空格都可以的,純粹看你排版習慣。

Flexbox vs Grid

很多開發者常常會問:Flexbox 不是跟 Grid 很相似嗎?

其實不是,它們不是競爭對手,而是互補的工具,而且它們幾乎是一起被設計出來的。雖然有些功能會重疊,但在不同的情境下,它們各有強項。到底什麼時候該用 Flexbox、什麼時候該用 Grid?可以從這兩個核心差異來判斷:

  • Flexbox 是一維的,只能在「橫的」或「直的」方向上排列;
  • Grid 是二維的,可以同時處理橫向和縱向的排列。

還有一個觀念可以幫助理解:Flexbox 是由內而外,從內容出發,它會根據項目的大小來決定怎麼排版;而 Grid 則是由外到內,先規劃好版型,再把內容填進去,比較像你先畫好格子,再把東西放進去。

如果只是要排列一組類似的東西,比如導覽列、按鈕列,那用 Flexbox 會比較直覺;但如果你要建立一個整體版型,比如整個網頁的區塊結構,Grid 就會更適合,因為它能讓你精準對齊不同區塊。

Flexbox 是從內容向外排版,而 Grid 是從排版結構向內安排內容

Flexbox 很適合用來排列一整行或一整列的元素,而且你不需要特別設定它們的寬度或高度,因為每個項目的大小會依照它的內容自動調整。簡單來說,就是內容有多少,空間就給多少。

Grid 的做法就不太一樣了。你是先定好整個版面怎麼切格子 (幾欄、幾列) ,再把內容塞進去。當然,內容還是會影響格子的大小,但這個變動會影響整個列或整欄,也就是說,一個格子的內容變多,會影響跟它同一列或同一欄的其他項目。

但像是導航選單這種比較不需要精確對齊的項目,其實可以讓內容來決定寬度。比方說文字多的連結寬一點、文字少的就窄一點,這就是典型的「一維水平佈局」。在這種情況下,用 Flexbox 會比 Grid 更合適。

如果你的設計需要在橫向和縱向兩個方向上都安排項目,那就選 Grid;但如果你只需要在單一方向上排列項目,像是一列或一行,那就用 Flexbox 比較合適。

實際上,在大多數情況下 (雖然不一定總是這樣) ,Grid 比較適合用來處理整體版型的架構,例如整個頁面的區塊配置;而Flexbox 則很適合用來處理這些區塊裡面的內容排列,例如按鈕列、選單、圖片組合等等。

注意:無論是 Grid 還是 Flexbox,都可以防止子項目之間的邊界重疊 (Margin Collapsing),這點跟平常使用的區塊排版不太一樣。不過有時候,瀏覽器預設樣式會讓區塊之間看起來空隙太大,可以重新設定瀏覽器預設樣式 margin: 0,來避免間距過多的問題。

好用語法

到這邊為止,我們已經把 Grid 的基本概念都跑過一輪了。接下來要介紹另外兩種寫法:命名網格線 (Naming grid lines)命名網格區域 (Naming grid areas)

這兩種方式其實沒有誰比較好,主要看你的習慣。根據設計的情況,有時候用其中一種會比較直觀、比較容易看懂。

命名網格線 (Naming grid lines)

網格會有很多欄、很多行,單靠數字來追蹤每一條網格線,會讓人眼花撩亂。為了讓這件事變簡單一點,我們可以幫網格線取名,這樣就不需在記數字,換成語意清楚的名稱來表示位置。做法很簡單:在你定義網格行或列的時候,把名稱放在中括號裡。

1
grid-template-columns: [start] 2fr [center] 1fr [end];

也就是建立一個有兩欄的網格,中間會有三條垂直的網格線,分別叫做 startcenter、和 end。之後在放項目的時候,就可以這樣寫:

1
grid-column: start / center;

這樣就代表這個項目從 start 線開始,到 center 線結束,比起寫 1 / 2,更容易理解。

另外也可以給同一條網格線設定多個名稱

1
2
3
4
grid-template-columns: 
[left-start] 2fr
[left-end right-start] 1fr
[right-end];

這樣的設定第 2 個網格線,可以同時有兩個名字:left-endright-start。你在放項目的時候,想用哪個名稱都可以。

更厲害的是,如果你使用 -start-end 這種命名方式,瀏覽器會自動把這兩個之間的區域理解成一個區塊。也就是說,當你定義了 left-startleft-end,就等於創造了一個叫做 left 的區域。之後就可以這樣寫:

1
grid-column: left;

那元素就會從 left-start 跨到 left-end,非常直覺!我把完整範例放在下方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.container {
display: grid;
grid-template-columns:
[left-start] 2fr
[left-end right-start] 1fr
[right-end];
grid-template-rows: repeat(4, [row] auto);
}
.item1 {
grid-column: left-start / right-end;
grid-row: span 1;
}
.item2 {
grid-column: left; /* 直接指定區域 */
grid-row: row 3 / span 2; /* `row 3` 代表第三個 row 的網格線 */
}

這個範例是用命名網格線來把每個項目放進對應的網格中。而且這個寫法還蠻有趣的:它在 repeat() 函數裡面也加上了水平網格線名稱。

這樣每一條水平網格線 (除了最後一條) 都會被命名。雖然一開始看起來有點怪怪的,但其實同一個名字可以重複使用,這在語法上是完全沒問題的。

row 3 代表從第 3 條同樣命名為 row 的水平網格線開始,然後 span 2 代表與下方軌道合併。

命名網格線的用法其實非常靈活,可以根據自己的排版需求來決定怎麼用。每個網格的結構不同,用法當然也會有所變化。

CSS Grid Naming

這個範例是一種重複的兩欄網格模式,重點在於在每對欄位前面命名網格線。

1
grid-template-columns: repeat(3, [col] 1fr 1fr);

這表示會重複三次這個組合:在一條叫 col 的網格線後面放兩欄。也就是說,你會有三組欄位,每組前面都有一條 col 的命名網格線。接下來,可以這樣放置元素:

1
grid-column: col 2 / span 2;

這行的意思是:從第二個名為 col 的網格線開始,然後橫跨兩欄。

命名網格區域 (Naming grid areas)

命名網格區域是另一種放置網格項目的方式,這種寫法更直觀,你不用去計算格子的編號,也不用命名一堆網格線,而是直接用區塊的名字來安排版面。做法很簡單:

  • 在網格容器上使用 grid-template-areas,來劃分出每個區域的位置。
  • 然後在每個網格項目上加上 grid-area,指派它要對應到哪個區域。

這種寫法算是一種語意化的替代語法,有時候會讓整體結構更容易看懂,特別是對大型版型來說。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.container {
display: grid;
grid-template-areas:
"title title"
"nav nav"
"main aside1"
"main aside2";
grid-template-columns: 2fr 1fr;
grid-template-rows: repeat(4, auto);
}
.title {
grid-area: title;
}
.nav {
grid-area: nav;
}
.main {
grid-area: main;
}
.aside1 {
grid-area: aside1;
}
.aside2 {
grid-area: aside2;
}

grid-template-areas 這個屬性可以在 CSS 裡用一種像是「ASCII」的方式排版,把整個網格畫出來。

我們將字串加上雙引號,每字串就代表網格裡的一行,然後用空格分隔每一個區域位置。看起來就像你在 CSS 裡畫出一塊區域。第一行可能整排都寫成 title title,代表整行都是標題;第二行寫成 nav nav 放導覽選單;接下來兩行,左邊的格子用 main,右邊分別用 aside1aside2,就能準確表示每個區塊位置。然後你只要在對應的項目加上:

1
grid-area: main;

這樣元素就會自動被放到 main 那一格去。不用再比對數字和線名,這種方式適合固定、清楚分布的版型設計,用來快速看懂整體排版很方便。

注意:每個命名網格區域都必須是個矩形。你不能用它來畫出像 L 形、U 形這種不規則的區塊,這是不被允許的。

如果你想要在某些地方留空白區域,也很簡單,只要在 grid-template-areas 的字串裡用句點 . 當作空白格子就可以。舉個例子,像下方這樣寫法:

1
2
3
4
grid-template-areas:
"top top right"
"left . right"
"left bottom bottom";

這段程式碼定義了四個區塊 (toprightleftbottom) ,中間那個格子留空,是用句點表示的。這樣的視覺結構看起來也很清楚,一眼就知道每個區域的位置。

總結一下:當你在做 Grid 佈局時,可以選擇三種方式來安排元素位置:

  • 編號網格線 Numbering Grid Lines
  • 命名網格線 Naming Grid Lines
  • 命名網格區域 Naming Grid Areas

這三種方式都沒有對錯,選你最順手的就好。但個人比較喜歡使用命名區域,因為比較直觀清楚。

顯式和隱式網格 Explicit and implicit grid

有時候你不一定知道每個項目應該放在網格的哪個位置,像是,你可能有一大堆項目要處理,一個一個手動指定位置很麻煩,又或者這些項目是從資料庫來的,數量根本不確定。在這種情況下,與其指定每個位置,不如大致定義網格結構,然後讓瀏覽器自動幫你擺好,這時候我們就會用到所謂的「隱式網格」 (implicit grid)

你平常用 grid-template-columnsgrid-template-rows 來定義網格時,其實是在建立一個「顯式網格」 (explicit grid),就是你指定哪一欄、哪一列。

但如果有項目超出你定義的欄或列,瀏覽器就會自動幫你新增額外的軌道,也就是所謂的隱式軌道,這樣整個網格就會自動擴展來容納那些額外的項目。

如果某個項目被放到沒有事先定義的格子外面,瀏覽器就會自動幫你新增隱式網格,讓這個項目有地方可以放。它會一直加軌道,直到能夠完整容納這個項目為止。

CSS Grid Layout

上圖的網格只有明確定義一個軌道,但當有項目跑到「第二行」的位置時,瀏覽器就自動新增一條行軌道,來裝下多出來的元素。

在預設情況下,隱式網格軌道的大小是 auto,意思是它們會自動根據內容大小來調整。

不過,如果你想要更精確地控制這些隱式軌道尺寸,可以在網格容器上加上 grid-auto-columnsgrid-auto-rows,像這樣:

1
grid-auto-columns: 1fr;

這樣一來,所有自動新增的欄位都會套用這個大小。

隱式網格軌道不會影響負數網格線的行為。就算有隱式軌道,你用負數 -1 來對齊,它還是會從顯式網格的最底部或最右邊開始算,不會變。

攝影作品集

接下來,我們要實作一個新的頁面,來看看隱式網格在實務上有多好用!這次的範例是一個攝影作品集頁面,最後會像這樣:

CSS Grid Portfolio

我們只定義欄位 (columns) ,每一行都讓瀏覽器自動產生 (也就是隱式網格)。這樣好處是:你不需要知道有幾張圖,不管是 3 張、10 張還是 100 張,網格都會自動幫你排好。每當照片需要換行,就自動新增一行,完全不用手動調整。首先建立我們的 HTML,圖片是找線上隨機產生的圖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="portfolio">
<figure class="featured">
<img src="https://picsum.photos/seed/a/400/400" alt="random image" />
</figure>
<figure>
<img src="https://picsum.photos/seed/b/200/200" alt="random image" />
</figure>
<figure class="featured">
<img src="https://picsum.photos/seed/e/400/400" alt="random image" />
</figure>
<figure>
<img src="https://picsum.photos/seed/c/200/200" alt="random image" />
</figure>
<figure>
<img src="https://picsum.photos/seed/d/200/200" alt="random image" />
</figure>
<figure>
<img src="https://picsum.photos/seed/f/200/200" alt="random image" />
</figure>
</div>

我們用 .portfolio 的元素來當作網格容器,裡面放了一系列 <figure>,每個 <figure> 都有一張圖片 <img>,這些 <figure> 就是網格項目。其中幾項上加上了 .featured 的 class,我們會讓它們在版面上看起來比其他的圖大一點,做出視覺重點。

我們分幾個步驟來完成作品集的排版:一開始,先定義好網格軌道,讓所有圖片整齊排列;接著,再把 .featured 放大;最後做一些調整和收尾,讓整體看起來更完整。

我們使用 grid-auto-rows,把所有隱式網格行高都設成 1fr,也就是每一行的高度都一樣,平均分配。這段 CSS 也引入了兩個新概念:auto-fillminmax() 函數,我等等再詳細解釋它們的用法。現在,先把清單裡的 CSS 加進去,我們再來看看這些新語法是怎麼用的。

CSS Grid Portfolio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
body {
background-color: #f1dbba;
font-family: Helvetica, Arial, sans-serif;
}
.portfolio {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 1fr;
grid-gap: 1em;
}
.portfolio > figure {
margin: 0;
}
.portfolio img {
max-inline-size: 100%;
}

有時候你不想幫網格軌道設定固定寬度,而是希望它在某個最小值跟最大值之間變動,這時候就可以用 minmax() 函數。這個函數的寫法是 minmax(最小值, 最大值),它會告訴瀏覽器:「這個軌道的大小不能小於某個值,也不能大於某個值」。舉例來說,如果你寫: minmax(200px, 1fr)。那瀏覽器就會保證每個欄位至少有 200px 寬,如果還有空間,就讓它們彈性變寬,最多佔 1fr 的比例空間。

另外還有一個關鍵字叫 auto-fill,是用來搭配 repeat() 函數的特別值。它的意思是:「根據你設定的寬度限制,自動塞滿整個容器能放下的欄數」。也就是說,如果你這樣寫:grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))。瀏覽器就會自動放入盡可能多的欄位,而且每一欄最少寬 200px,最多寬不超過 1fr,這樣每個欄位就會平均分配空間,而且不會太小。

這種搭配很好用,尤其在做 RWD (響應式網頁) 時,可以讓你的格子根據螢幕自動調整數量與大小,不需要寫一堆 @media query

如果螢幕寬度剛好能放下 4 欄、每欄 200px 寬的網格,瀏覽器就自動幫你建立了 4 個欄位。如果螢幕更寬,能放更多,瀏覽器就會多加幾欄;反過來,如果螢幕比較窄,那欄位數也會自動變少。

但要注意:使用 auto-fill 的時候,有可能會出現空的網格欄位。比方說,你的畫面可以放 6 欄,但實際上只有 4 個項目,那剩下的 2 欄就會變成空的格子,還是照樣被排出來。

CSS Grid Auto Fill vs Auto Fit

如果你不想出現這種空的格子,那可以改用 auto-fit 來取代 auto-fillauto-fit 的意思是:「把有內容的欄位拉寬來填滿整個容器」,這樣就不會留下空的欄位,看起來更緊湊。

  • auto-fill:你會得到預期數量的欄位大小,不管有沒有東西填進去;
  • auto-fit:會讓有內容的欄位自動撐開,讓整個容器看起來是滿的,不會有多餘空格。

用哪一個,取決你是想要固定欄數,還是想要讓畫面填滿得漂亮。

增加變化:將 .feature 放大

接下來,我們可以視覺效果更吸引人一點:把 .feature 圖片放大。目前每個網格項目都只佔 1x1 的格子,也就是一欄一列。現在我們要讓 .featured 的項目放大到佔兩欄、兩列,也就是 2x2 的區塊。用 .featured 來鎖定這些項目,然後讓它們在水平和垂直方向上各跨兩個軌道,就能達成放大的效果。

CSS Grid Porfolio

不過這樣做會遇到一個小問題:當你讓某些項目變大時,根據項目的順序,有可能會在網格中產生空隙 (如右上角)。因為有些項目原本預期只佔一格,突然變大後可能會擠到別的項目,結果就出現一些空白。

在這個圖裡,因為網格裡的第 3 個項目放大了,要佔兩格寬和高,所以沒辦法塞進右上角的空間裡,結果它就被擠到下一行。為什麼會這樣?因為當你沒特別指定每個項目的位置時,瀏覽器會使用內建的「網格項目自動放置演算法 (Grid item placement algorithm)」來自動幫你排列。

這個演算法的預設行為是:按照 HTML 裡項目的順序,從左到右、從上往下放。但如果某個項目太大,放不下當前這一行,它就會被移到下一行,去找能放得下的空間。所以在這個例子中,第三個項目就被移到下方,因為那邊的位置才夠。

不過好消息是:CSS Grid 有提供一個屬性叫做 grid-auto-flow,可以用來控制這個排版邏輯。預設值是 row,就是我們剛剛說的「先按列再換行」;如果你設定成 column,那它就會變成「先按行再換列」;更進一步,你可以加上 dense,像這樣:

1
grid-auto-flow: row dense;

這樣做的效果是:演算法會盡量把小的項目「補進」那些因為大項目被跳過的位置。就算會讓項目呈現的順序和原本 HTML 的順序不完全一樣,它也會盡可能把版面填滿,不留空白。

現在,我們來更新一下樣式表:

1
2
3
4
5
6
7
8
9
10
11
.portfolio {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: 1fr;
gap: 1em;
grid-auto-flow: dense;
}
.portfolio .featured {
grid-row: span 2;
grid-column: span 2;
}

這段樣式使用了 grid-auto-flow: dense,其實等同於寫成 grid-auto-flow: row dense,因為 row 是預設值,就算省略也一樣有效。接著,鎖定 .featured 項目讓它在水平方向和垂直方向上各跨兩格,也就是:

CSS Grid Porfolio

注意這個範例只用 span 關鍵字來設定大小,並沒有明確指定項目要放在哪一格。這樣的寫法可以讓瀏覽器的「網格項目自動放置演算法自行決定這些項目要排在哪裡,不用特別指定位置,也避免出現錯位或衝突。

根據你螢幕寬度,你看到的畫面可能會跟上圖完全不一樣。這是因為我們用了 auto-fill 來決定垂直網格軌道的數量。如果你的螢幕比較寬,就能容納更多欄;如果螢幕比較窄,就只會顯示比較少的欄位。

我自己是用大約 1000px 左右的視窗來截圖,所以畫面上剛好出現了 4 欄。如果調整瀏覽器的寬度,網格就會自動適應空間變化,這就是 Grid 的厲害之處!

當你使用 grid-auto-flow: dense 時,畫面上項目的排列順序,可能跟 HTML 原始碼的順序不一樣。對大多數使用者沒差,但對鍵盤 (像是按 Tab 鍵) 或用螢幕閱讀器的使用者來說可能會有點混亂,因為這些輔助工具是根據原始碼的順序來瀏覽,不是照畫面上看到的順序。所以如果你很在意無障礙體驗 (accessibility) ,在使用 dense 這個選項時要特別小心,確保不會造成使用上的混淆。

微調

現在我們已經完成一個複雜的網格佈局,而且還不需要指定每個項目位置,瀏覽器自動幫你排好,非常省力。不過最後還有一個小問題要解決:你會發現有些圖片放大 (.featured項目) 並沒有完全填滿它所在的網格格子,導致下面或右邊多出一點小空隙。理想情況下,同一個網格軌道裡的所有項目,上緣和下緣都應該要對齊,這樣版面才整齊。目前我們的圖片上緣對齊沒問題,但下緣就有點歪。

CSS Grid Porfolio

我們來把空隙修正一下,還記得每個網格項目其實是一個 <figure> 元素,裡面有一張圖片:

1
2
3
<figure class="featured">
<img src="..." alt="monkey" />
</figure>

預設情況下,每個 <figure> (也就是網格項目) 會自動撐滿它被分配到的整個格子,但網格項目裡的子元素 (像圖片) 並不會自動撐滿整個高度。結果就是:<figure> 佔滿了格子,但 <img> 還是保留原來的大小,所以下面就出現一段空白。

要修正這種情況,我們可以用 Flexbox 來解決圖片下方出現空隙的問題。我們可以把每個 <figure> 設成一個彈性容器,接著,對圖片設定 flex-grow,這樣它就會撐滿剩下的空間,不會是原本圖片的高度,下面也就不會有空白了。

但這樣會遇到另一個問題:圖片被強行拉伸的時候,寬高比例會跑掉,導致變形。CSS 有一個專門處理圖片拉伸的屬性叫做 object-fit。預設情況下,<img>object-fitfill,也就是整張圖片會撐滿 <img> 這個框框,不管比例對不對。如果你想讓圖片保持原來的比例,那可以改用這兩個設定:

  • cover:讓圖片放大到填滿整個容器,即使有一部分被裁掉也沒關係。
  • contain:讓整張圖片完整顯示在容器裡,但容器裡可能會留一些空白。

下圖展示這些效果的差別,選擇哪一種取決於你希望圖片被裁切掉,還是要完整顯示。

CSS Grid Image Object Fit

這裡有一個很重要的觀念要釐清:<img> 有兩個層次的尺寸要分開看:

  • 一個是外框大小,也就是 <img> 元素本身的 widthheight
  • 另一個是圖片內容大小,透過 object-fit 設定圖片在框裡面怎麼呈現。

預設情況下,這兩者大小是相同的。但透過 object-fit 可以控制圖片在框裡的呈現方式,而不會因此改變框的大小。

我們剛才用 flex-grow 讓圖片伸展去填滿整個網格格子,然後再加上 object-fit: cover,讓圖片避免拉扯變形。cover 會讓圖片保持比例,並撐滿整個外框,雖然會裁掉圖片的邊角,但這是我們在這裡所做的取捨。

1
2
3
4
5
6
7
.portfolio > figure {
display: flex;
}
.portfolio img {
flex: 1;
object-fit: cover;
}

CSS Grid Porfolio

Subgrid

前面範例中,我們的網格結構都只侷限在 DOM 的兩個層級內:一個網格容器 (parent) 加上它的直接網格項目子元素 (children) 。不過在某些設計中,你可能會遇到更複雜的需求,像是:你需要對齊不在同一個父層底下的元素;或者你希望兩個元素,雖然不在同一層,卻能有一樣的大小或對齊方式。舉個例子:你可能想要讓兩個相同祖先的內容區塊,都能垂直對齊或寬度一致,無論內容多少。

CSS Grid Subgrid

在這個設計裡,每張卡片本身就是網格的一部分,所以它們會整齊地排成一列,並且高度一致。但不只這樣,卡片裡面的內容也會彼此對齊。像有些卡片的標題比較短,這時就會在上方加一些空間,讓它跟標題比較長的卡片對齊;每段內文的開頭也都對齊在同一高度,最底下的「Read More」連結也都整齊排在同一條水平線上。要做到內層與外層內容同步對齊的效果,最理想的方法是用新的 CSS 功能:子網格 (subgrid)

子網格的好處是:可以在一個網格容器裡再建立一個內層網格,並且讓這個內層網格的項目直接對齊外層網格的線條。這樣就可以讓整體看起來更一致。

在這個頁面中,每張卡片會跨三行,而卡片裡的標題、段落、按鈕也剛好對應到那三行的位置。不管是卡片本身還是卡片裡的內容,都是依照同一組網格線排列。

CSS Grid Subgrid

子網格 Subgrid 是一個比較新的 CSS 功能,已經支援大多數瀏覽器。你可以到這個網站查看最新的支援狀況:https://caniuse.com/css-subgrid

我們先建立一個最基本的卡片和樣式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div class="author-bios">     
<div class="card">
<h3>Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Long Header</h3>
<div>
<p>
Lorem Ipsum
</p>
</div>
<div class="read-more"><a href="#">Read more</a></div>
</div>
<div class="card">
<h3>Header</h3>
<div>
<p>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum
</p>
</div>
<div class="read-more"><a href="#">Read more</a></div>
</div>
<div class="card">
<h3>Header</h3>
<div>
<p>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
</p>
</div>
<div class="read-more"><a href="#">Read more</a></div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
body {
background-color: #eee;
font-family: Helvetica, Arial, sans-serif;
}
.author-bios {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1em;
max-inline-size: 800px;
margin-inline: auto;
}
.card {
padding: 1rem;
border-radius: 0.5rem;
background-color: #fff;
}
.card > h3 {
margin-block: 0;
padding-block-end: 0.5rem;
border-block-end: 1px solid #eee;
}
.read-more {
margin-block-start: 1em;
text-align: right;
}

CSS Grid Subgrid

現在樣式加上去後,已經接近你想要的設計。不過,還差最後一步:我們來用子網格,讓卡片裡的內容也能跟整個網格對齊一致。

要在頁面上套用子網格,需要做兩件事:先在每個網格項目 (也就是卡片) 上加上 display: grid,這樣就能在每張卡片裡建立一個內層網格;然後,在內層網格的 grid-template-rowsgrid-template-columns 上設定 subgrid,這表示:這個內層網格要使用父層網格的網格來對齊

這樣一來,卡片裡的內容 (標題、文字、按鈕) 就會直接對齊其他卡片內容,達到完全一致的視覺效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.card {
display: grid;
gap: 0;
grid-row: span 3;
grid-template-rows: subgrid;
padding: 1rem;
border-radius: 0.5rem;
background-color: #fff;
}
.card > h3 {
align-self: end;
margin-block: 0;
padding-block-end: 0.5rem;
border-block-end: 1px solid #eee;
}

因為子網格是比較新的功能,有些瀏覽器可能還不支援。如果你想要更保險一點,也可以用 CSS 的功能查詢 (feature query) 來判斷瀏覽器是否支援 subgrid,像這樣寫:

1
2
3
@supports (grid-template-rows: subgrid) {
/* 只有支援 subgrid 的瀏覽器才會套用這段樣式 */
}

另外也可以用 grid-template-columns: subgrid 來對齊列 (columns),但前提是父層有定義網格。你也可以列和欄一起使用 subgrid,讓內部區塊完整對齊父容器。

更進一步,你還可以把這個結構一層一層往下套,也就是在一個子網格裡面再建立另一個子網格,讓整個 DOM 結構中的區塊都能保持一致的網格對齊,非常適合用在大型或模組化的設計裡。

subgrid 中,有一個很棒的特性是:你可以繼承父網格的所有線號 (line numbers) 、線名稱 (line names) 和區域名稱 (grid area names) ,這代表你可以直接用這些資訊來擺放子網格中的元素。舉個例子,假設你想要把卡片中的標題全部放在第二行,你可以這樣寫:

1
2
3
.card > h3 {
grid-row: 2;
}

除了可以繼承父網格的線名稱,你也可以在 subgrid 裡定義自己的線名稱,即使父網格沒有那些名稱:

1
2
3
.card {
grid-template-rows: subgrid [line1] [line2] [line3] [line4];
}

這樣就能使用自訂的線名稱來放置元素。grid-row: line3 / line4; 這表示你要把項目放在第 3 條和第 4 條橫向網格線之間的位置。

Masonry

補充小知識,未來 CSS 可能會加入新功能叫做「masonry layout (瀑布流排版) 」,這種排版方式在圖片集 (像 Pinterest) 裡很常見,但目前要靠 JavaScript 才能實現。

CSS Grid Masonry

Masonry 的特色是:它會把項目放在等寬的欄位中,但每個項目的高度可以不一樣,排版就不會要求每一列都整齊對齊,能更靈活地呈現不同大小的內容。

你可以到這個網站查看最新的支援狀況 https://caniuse.com/?search=masonry

對齊屬性

在 CSS Grid 模組中,我們會用到一些跟 Flexbox 很像的對齊屬性,另外還多了幾個新的。我們其實已經在 Flexbox 介紹過大部分,但有一些屬性是只有在 Grid 裡才有作用的。當你需要更細緻地控制網格排版時,這些對齊屬性就會派上用場。

首先是三個 justify 開頭的屬性,這些是用來 控制水平方向 (橫向) 的排列。接著是三個 align 開頭的屬性,用來控制垂直方向 (直向) 的排列

CSS Grid Alignment Property

最後補充一點:CSS 還提供了簡寫屬性,可以同時設定 align-*justify-* 的值:

  • place-content:同時設定 align-contentjustify-content
  • place-items:同時設定 align-itemsjustify-items
  • place-self:同時設定 align-selfjustify-self

Reference