Masonry Grid
響應式瀑布流佈局並動畫重排,卡片隨欄數與資料變化平滑補位。
以絕對定位排入最矮的欄,形成高低錯落的瀑布流。欄數隨容器寬度改變,資料增刪或順序調換時每張卡都會平滑補位。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add masonry-gridMasonry Grid 是用 React、Tailwind CSS 與 Motion 做的瀑布流網格(masonry layout、Pinterest 式排版)元件:每張卡片依序放進目前最矮的一欄,欄數依容器寬度變化,資料增刪、排序或欄數改變時,卡片會以動畫移到新位置。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<MasonryGrid/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 攝影作品集、設計靈感牆這類圖片高低不一的圖庫
- 部落格或知識庫的文章卡片牆,摘要長短不同
- 使用者評價、社群貼文的牆面展示
- 可依標籤篩選的作品列表,篩選時卡片平滑補位
- 不適合:卡片需要跨欄、版面固定的功能總覽,改用 Bento Grid
安裝
npx webberui@latest add https://webberui.com/r/masonry-grid.json或在 components.json 設定 registries 後,改用 @webberui/masonry-grid 安裝。
使用
import { MasonryGrid, type MasonryItem } from "@/components/ui/masonry-grid";
const items: MasonryItem[] = [
{ id: "a", height: 200, className: "bg-sky-500" },
{ id: "b", height: 140, className: "bg-rose-500" },
{ id: "c", height: 260, className: "bg-emerald-500" },
];
<MasonryGrid items={items} columns={{ 0: 1, 640: 2, 1024: 3 }} gap={16} />;columns 可以是固定數字(例如 3),也可以是「容器寬度斷點對照表」:鍵是容器最小寬度(px),值是該寬度以上的欄數。斷點依「容器寬度」而非視窗寬度判定,因此放在側欄或彈窗中也能正確排版。
每筆 item 的 height 決定卡片高度,寬度則由欄寬自動計算;把內容放進 content,或用 className 直接套漸層背景。
範例
圖片作品集
卡片高度由 height 決定,內容不會撐開卡片。圖片用 object-cover 填滿,高度可以依原圖比例預先算好放在資料裡。
import { MasonryGrid, type MasonryItem } from "@/components/ui/masonry-grid";
const photos = [
{ id: "p1", src: "/works/cafe.jpg", alt: "咖啡店室內設計", height: 320 },
{ id: "p2", src: "/works/poster.jpg", alt: "展覽海報", height: 420 },
{ id: "p3", src: "/works/app.jpg", alt: "App 介面設計", height: 240 },
{ id: "p4", src: "/works/brand.jpg", alt: "品牌識別", height: 360 },
];
const items: MasonryItem[] = photos.map((p) => ({
id: p.id,
height: p.height,
content: <img src={p.src} alt={p.alt} className="h-full w-full object-cover" />,
}));
export function Portfolio() {
return <MasonryGrid items={items} gap={12} />;
}依標籤篩選
items 改變時,留下的卡片會從原位置移到新座標,以 id 追蹤同一張卡片,所以 id 必須穩定且唯一。
"use client";
import * as React from "react";
import { MasonryGrid, type MasonryItem } from "@/components/ui/masonry-grid";
type Post = MasonryItem & { tag: "設計" | "開發" };
export function FilterableWall({ posts }: { posts: Post[] }) {
const [tag, setTag] = React.useState<"全部" | Post["tag"]>("全部");
const visible = tag === "全部" ? posts : posts.filter((p) => p.tag === tag);
return (
<>
<div className="mb-4 flex gap-2">
{(["全部", "設計", "開發"] as const).map((t) => (
<button key={t} onClick={() => setTag(t)} aria-pressed={tag === t}>
{t}
</button>
))}
</div>
<MasonryGrid items={visible} />
</>
);
}側欄裡的固定兩欄
columns 傳數字就固定欄數;animateFrom="center" 搭配關閉模糊,進場只剩淡入與輕微放大。
<aside className="w-80">
<MasonryGrid
items={items}
columns={2}
gap={8}
animateFrom="center"
blurToFocus={false}
scaleOnHover={false}
/>
</aside>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
items | MasonryItem[] | — | 網格項目資料 |
columns | number | Record<number, number> | { 0: 1, 480: 2, 768: 3, 1024: 4 } | 固定欄數或響應式斷點對照表 |
gap | number | 16 | 卡片間距(px) |
duration | number | 0.5 | 單張卡片動畫時長(秒) |
stagger | number | 0.05 | 進場時每張卡的間隔(秒) |
animateFrom | "bottom" | "top" | "left" | "right" | "center" | "bottom" | 進場起始方向 |
blurToFocus | boolean | true | 進場時是否由模糊到清晰 |
scaleOnHover | boolean | true | 懸停時是否輕微放大卡片 |
ease | [number, number, number, number] | [0.22, 1, 0.36, 1] | 動畫緩動曲線 |
MasonryItem
| 欄位 | 型別 | 說明 |
|---|---|---|
id | string | 唯一識別,重排動畫以此追蹤同一張卡 |
height | number | 卡片高度(px),寬度由欄寬自動決定 |
content | React.ReactNode | 卡片內容;未提供時渲染佔位面板 |
className | string | 附加在卡片外框上的樣式 |
可及性
- 使用者系統開啟「減少動態效果」時,卡片直接就位、不播放進場與重排動畫
- 排版於客戶端量測容器寬度後才渲染,避免 SSR 與客戶端不一致(hydration mismatch)
- 以
ResizeObserver監看容器寬度,離開時自動斷開,尺寸變化即時重排
常見問題
卡片高度可以依內容自動決定嗎?
不行。卡片是絕對定位,位置需要事先算出,所以每筆資料都要提供 height(px)。圖片可以依原圖寬高比與預期欄寬估算高度,或用 object-cover 裁切。
伺服器端會輸出卡片內容嗎?
不會。元件在客戶端量測到容器寬度後才渲染卡片,伺服器端只輸出空的容器。內容需要被搜尋引擎索引時,請另外在頁面上提供可抓取的連結或列表。
排列順序是怎麼決定的?
依 items 的順序逐一放進當下最矮的一欄。因此畫面上的位置不一定是從左到右、從上到下依序排列,鍵盤 Tab 的順序則與 items 陣列一致。
欄數斷點是看視窗還是容器?
看容器。columns 的對照表以 ResizeObserver 量到的容器寬度判斷,預設是 0px 一欄、480px 兩欄、768px 三欄、1024px 四欄。
可以在 Next.js App Router 使用嗎?
可以。原始碼已經有 "use client";要從 Server Component 傳入 items 時,content 裡的 JSX 也可以由伺服器端產生。依賴見上方安裝區塊。