卡片與網格

Masonry Grid

響應式瀑布流佈局並動畫重排,卡片隨欄數與資料變化平滑補位。

以絕對定位排入最矮的欄,形成高低錯落的瀑布流。欄數隨容器寬度改變,資料增刪或順序調換時每張卡都會平滑補位。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add masonry-grid

還沒有專案,或安裝遇到問題?看快速開始 →

Masonry Grid 是用 React、Tailwind CSS 與 Motion 做的瀑布流網格(masonry layout、Pinterest 式排版)元件:每張卡片依序放進目前最矮的一欄,欄數依容器寬度變化,資料增刪、排序或欄數改變時,卡片會以動畫移到新位置。

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

3
16
0.05
編輯中的呼叫預設屬性
<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型別預設值說明
itemsMasonryItem[]—網格項目資料
columnsnumber | Record<number, number>{ 0: 1, 480: 2, 768: 3, 1024: 4 }固定欄數或響應式斷點對照表
gapnumber16卡片間距(px)
durationnumber0.5單張卡片動畫時長(秒)
staggernumber0.05進場時每張卡的間隔(秒)
animateFrom"bottom" | "top" | "left" | "right" | "center""bottom"進場起始方向
blurToFocusbooleantrue進場時是否由模糊到清晰
scaleOnHoverbooleantrue懸停時是否輕微放大卡片
ease[number, number, number, number][0.22, 1, 0.36, 1]動畫緩動曲線

MasonryItem

欄位型別說明
idstring唯一識別,重排動畫以此追蹤同一張卡
heightnumber卡片高度(px),寬度由欄寬自動決定
contentReact.ReactNode卡片內容;未提供時渲染佔位面板
classNamestring附加在卡片外框上的樣式

可及性

  • 使用者系統開啟「減少動態效果」時,卡片直接就位、不播放進場與重排動畫
  • 排版於客戶端量測容器寬度後才渲染,避免 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 也可以由伺服器端產生。依賴見上方安裝區塊。