[Web] Grid + JS 瀑布流實作
/ 7 min read
Updated: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 時,後面的項目會被塞回前面的空洞,視覺順序可能被打亂
瀑布流常見取捨如下
- 要密度會犧牲順序
- 要順序會犧牲密度
方案比較
- 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
- 排序特性:順序保留,同時盡量補洞
Grid + JS 的實作方式
我後來是參照 css-tricks 的一篇文章解決這個需求,節錄幾個我覺得重要的片段
Approaches for a CSS Masonry Layout
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 普及後,才會是瀑布流真正的原生解