Column Transit Board
跨欄轉運看板:卡片因程式狀態改變(非拖曳)沿弧線飛越欄位,目標欄先撐開落點、原欄鄰卡合攏、飛行卡帶投影抬升。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
0.6
<ColumnTransitBoard />
安裝
npx shadcn@latest add https://webberui.com/r/column-transit-board.json或在 components.json 設定 registries 後,改用 @webberui/column-transit-board 安裝。
安裝依賴後,從 registry JSON(/r/column-transit-board.json 的 files[0].content)複製 column-transit-board.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion clsx tailwind-merge飛行卡片的落點光暈使用 var(--wb-duration-fast,200ms) 作為調校 fallback,未定義該變數也能正常運作。
使用
卡片的欄位歸屬完全由你的狀態決定——把某張 TransitCard 在 JSX 中改放到另一個 TransitColumn,看板就會偵測到換欄並播放飛行動畫。這是純受控元件,不持有任何內部佈局狀態。
import * as React from "react";
import {
ColumnTransitBoard,
TransitColumn,
TransitCard,
} from "@/components/ui/column-transit-board";
const COLUMNS = [
{ id: "todo", title: "待處理" },
{ id: "doing", title: "進行中" },
{ id: "done", title: "已完成" },
];
const [placement, setPlacement] = React.useState<Record<string, string>>({
a: "todo",
b: "doing",
});
<ColumnTransitBoard>
{COLUMNS.map((col) => (
<TransitColumn key={col.id} id={col.id} title={col.title}>
{Object.entries(placement)
.filter(([, colId]) => colId === col.id)
.map(([cardId]) => (
<TransitCard key={cardId} id={cardId} title={cardId} />
))}
</TransitColumn>
))}
</ColumnTransitBoard>;
// 換欄:改變狀態即觸發飛行
setPlacement((prev) => ({ ...prev, a: "doing" }));Props
ColumnTransitBoard
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 直接放入 TransitColumn 子元件,欄內再放 TransitCard |
flightDuration | number | 0.6 | 卡片跨欄飛行的弧線時長(秒) |
className | string | — | 附加到看板容器的 class |
TransitColumn
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
id | string | — | 欄位唯一識別 key,卡片換欄偵測與落點光暈定位用 |
title | React.ReactNode | — | 欄位標題(字串時同時作為 aria-label) |
accent | Accent | "neutral" | 欄位強調色,套用到標記點與落點光暈 |
children | React.ReactNode | — | 直接放入 TransitCard 子元件 |
className | string | — | 附加到欄位容器的 class |
TransitCard
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
id | string | — | 卡片唯一識別 key,須全板唯一且穩定(跨欄 FLIP 靠它配對) |
title | React.ReactNode | — | 卡片標題(未提供 children 時的預設內容) |
description | React.ReactNode | — | 卡片描述(未提供 children 時的預設內容) |
accent | Accent | "neutral" | 卡片強調色,套用到左側色條 |
children | React.ReactNode | — | 自訂卡片內容,提供時覆蓋 title/description |
className | string | — | 附加到卡片的 class |
Accent 為 "neutral" | "blue" | "violet" | "sky" | "emerald" | "amber" | "rose"。
細節
- 靠
id追蹤,不靠 DOM 節點:卡片換欄時 React 會在來源欄卸載、目標欄重掛,看板以id為 key 記錄換欄前後的位置(FLIP),因此重掛也能無縫接上飛行。 - 只有結構改變才重排:看板為每次渲染算出「欄位 → 卡片」的佈局指紋,指紋不變時完全不量測、不干擾進行中的動畫。
- 三段編排:換欄卡片沿拋物弧線飛越並抬升投影(
z-index浮到最上層);同欄鄰卡以較快的 spring 直線合攏補位,讓目標欄落點「先撐開」;目標欄同時浮現一圈落點光暈。 - 飛行途中若再次換欄:前一段飛行會即時收束到落點,再從新起點重新編排,狀態保持一致。
可及性
- 使用者系統開啟「減少動態效果」時,卡片直接就位、不播放飛行,佈局結構不變。
- 看板為
role="group",每欄為role="list"(以標題作aria-label),卡片為role="listitem"。 - 落點光暈為純裝飾層(
aria-hidden、pointer-events-none),不影響佈局量測與互動。