skip to content
BlogZzz
Table of Contents

前言

這篇整理我在做瀑布流時的思路,主軸是 Grid 搭配 JS 的實作方式 最後簡單提到 grid-lanes

需求與觀察

希望排版由左至右、由上而下,視覺順序和 DOM 順序一致 看了 unsplash.com、pexels.com、kaboompics.com 的呈現方式,最接近需求的是 kaboompics 也在掘金看到 滚动的魅力:如何实现大厂也在用的瀑布流布局,想弄清楚實作細節

Grid 的限制

Grid 能控制欄位,但仍是規則網格

  • 項目高度不一致時仍按行列對齊
  • 空洞不會自動被後面的項目填補

grid-auto-flow: dense 可以補洞,但會改變視覺順序 只有在項目有 span 時,瀏覽器才有機會回填空洞

  • 沒有 span 時,結果接近一般 Grid,空洞仍在
  • 有 span 時,後面的項目會被塞回前面的空洞,視覺順序可能被打亂
demo

瀑布流常見取捨如下

  • 要密度會犧牲順序
  • 要順序會犧牲密度

方案比較

  • Column flow
    • 方式:CSS Multi‑column
    • 主要屬性:column-count、column-gap、break-inside: avoid
    • 排序特性:欄優先,先把左欄由上往下塞滿,再往右欄
  • Order-preserving grid
    • 方式:CSS Grid
    • 主要屬性:display: grid、grid-template-columns、grid-auto-rows、gap、align-items: start
    • 排序特性:列優先,由左到右、由上到下,順序和 DOM 一致,但高度不同會留下空洞
  • Grid + JS
    • 方式:JS 量測高度並指定 row span
    • 排序特性:順序保留,同時盡量補洞
demo

Grid + JS 的實作方式

我後來是參照 css-tricks 的一篇文章解決這個需求,節錄幾個我覺得重要的片段

Approaches for a CSS Masonry Layout

demo3

1) resizeMasonryItem(item):計算每張卡片要跨幾列

主流程分成三步:

function resizeMasonryItem(item: HTMLDivElement) {
// (1) 讀取 Grid 容器的參數
// 這裡抓了三個關鍵值:
// - grid-row-gap:列與列之間的間距
// - grid-auto-rows:每個隱式 row track 的高度
// - grid:整個 masonry container
//
// 為什麼要這些值?
// 因為要把「內容高度」換算成「需要跨幾個 row track」,才能用 grid-row-end: span X 表示
let grid = document.getElementsByClassName("masonry")[0],
rowGap = parseInt(
window.getComputedStyle(grid).getPropertyValue("grid-row-gap"),
),
rowHeight = parseInt(
window.getComputedStyle(grid).getPropertyValue("grid-auto-rows"),
);
// (2) 取得內容高度,換算 rowSpan,這段是核心公式:
// - H:內容高度
// - G:row gap
// - R:row height
// - rowSpan = ceil((H + G) / (R + G))
// 也就是:實際內容高度 + gap,除以每個 row track + gap 的高度,得到需要跨幾列
let rowSpan = Math.ceil(
(item.querySelector(".masonry-content").getBoundingClientRect().height +
rowGap) /
(rowHeight + rowGap),
);
// (3) 寫回 CSS Grid
item.style.gridRowEnd = "span " + rowSpan;
item.querySelector(".masonry-content").style.height = rowSpan * 10 + "px";
}

2) resizeAllMasonryItems():一次處理所有項目

這個函式理論上是用來把所有 .masonry-item 一次跑過:

let allItems = document.getElementsByClassName("masonry-item");
for (let i = 0; i < allItems.length; i++) {
resizeMasonryItem(allItems[i]);
}

但我自己的專案是在每個圖片的 onLoadingComplete 單張呼叫 resizeMasonryItem,可以自行調整適合自己的方式


3) 一個值得注意的細節

item.querySelector(".masonry-content").style.height = rowSpan * 10 + "px";

可以嘗試把這段移除會發現圖片底部會有空白

為什麼會有空白:

  • rowSpan 是用 Math.ceil((H + G) / (R + G)) 算出來的,所以 Grid 區塊高度會被「向上取整」到某個階梯值
  • 圖片本身高度是實際像素,通常 略小於 那個階梯值,所以 Grid 區塊底下會留一條空白

那行的作用:

  • rowSpan * 10 等於把圖片高度「硬拉到計算後的階梯高度」,再配合 .masonry-item { overflow: hidden; },多出的部分被裁掉,看起來就不會有底部空白 它確實有用,但它是一個 硬編 10px 的補洞手法,等於假設了「每一格=10px」,這跟目前 grid-auto-rows: 0 的設定其實不一致,只是剛好因為 grid-row-gap 也是 10px,所以看起來還能對上

如果你要更穩定的解法,核心思路是:

  • 讓「計算 rowSpan 的單位」跟「圖片最後被設的高度單位」一致
  • 例如用 rowHeight 或 rowGap 來算,而不是寫死 10

結論:

  • 它把「內容高度」換算成「Grid 可以理解的 row span」
  • 這就是用 CSS Grid + JS 做 Masonry 的典型解法
  • 它不是原生 Masonry,但能在現有瀏覽器得到接近的效果

延伸閱讀

grid-lanes 為什麼值得期待

display: grid-lanes 是 CSS Grid Level 3 的草案,目標是原生支援 Masonry

瀏覽器會把項目放進最短的 lane,密度高且保留 DOM 順序

延續 Grid 的語法(如 grid-template-columns),但排版方式不再是傳統「列對齊」,而是由瀏覽器把每個項目放到「最接近頂部」的 lane 中,自動補洞、緊密堆疊

這意味著瀑布流不再需要 JS 量測高度或用 grid-row-end: span X 來模擬,排版更接近原生、可維護

目前只有 Safari Technology Preview 有實驗支援,其餘瀏覽器仍在旗標或轉譯階段

因為支援度仍低,生產環境目前多採「Grid + JS」或套件方式,但可用 display: grid; display: grid-lanes; 做漸進增強,讓支援瀏覽器享受原生瀑布流,其餘仍回退到 Grid

現階段可用 Grid + JS 作為主方案,並在支援度提升後逐步改成原生

結語

Grid + JS 不是最優雅的解法,但能在現階段兼顧順序與密度 如果需求是內容高度差異大,又不能改變順序,這個作法值得採用 等 grid-lanes 普及後,才會是瀑布流真正的原生解