Miller Column Browser
Finder 式米勒欄層級瀏覽版面,每深入一層就從右側彈簧推入一欄,超出寬度時整排平滑橫移,並以麵包屑同步高亮當前路徑。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
240
<MillerColumnBrowser />
安裝
npx shadcn@latest add https://webberui.com/r/miller-column-browser.json或在 components.json 設定 registries 後,改用 @webberui/miller-column-browser 安裝。
安裝依賴後,從 registry JSON(/r/miller-column-browser.json 的 files[0].content)複製 miller-column-browser.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion lucide-react clsx tailwind-merge使用
傳入一棵 MillerNode 樹(items)即可。有 children 的節點是資料夾,點選會往右推入一欄顯示其子項;沒有 children 的是葉節點,選取後只高亮、不再展開。麵包屑會同步反映目前路徑,點任一層即可跳回。
import {
MillerColumnBrowser,
type MillerNode,
} from "@/components/ui/miller-column-browser";
const items: MillerNode[] = [
{
id: "design",
label: "設計系統",
children: [
{ id: "color", label: "色彩", children: [{ id: "neutral", label: "中性色" }] },
{ id: "type", label: "字體" },
],
},
{ id: "docs", label: "文件" },
];
<div className="h-[320px]">
<MillerColumnBrowser items={items} rootLabel="資源庫" />
</div>;每個節點是一個 MillerNode(id / label,選配 children / icon / meta / disabled)。children 可任意巢狀,形成任意深度的欄。
受控模式
傳入 path 與 onPathChange 即進入受控模式,由外部完全掌握目前選取路徑(由根到當前的節點 id 陣列);不傳則用 defaultPath 的非受控模式(內部自管狀態)。
const [path, setPath] = React.useState<string[]>(["design", "color"]);
<MillerColumnBrowser
items={items}
path={path}
onPathChange={(next) => setPath(next)}
/>;監聽葉節點選取
onLeafSelect 只在選到沒有子項的節點時觸發,適合在選定最終項目時載入詳情或觸發導覽。
<MillerColumnBrowser
items={items}
onLeafSelect={(node, path) => console.log("已選取", node.id, path)}
/>Props
MillerColumnBrowser
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
items | MillerNode[] | — | 階層資料的根層項目 |
path | string[] | — | 受控模式:目前選取路徑(節點 id 陣列) |
defaultPath | string[] | [] | 非受控模式:初始選取路徑 |
onPathChange | (path: string[], node: MillerNode | null) => void | — | 選取路徑變更時的回呼 |
onLeafSelect | (node: MillerNode, path: string[]) => void | — | 選到葉節點(無子項)時的回呼 |
breadcrumbs | boolean | true | 是否顯示頂端麵包屑列 |
rootLabel | React.ReactNode | "根目錄" | 根層在麵包屑顯示的標籤 |
columnWidth | number | 240 | 每欄寬度(px) |
emptyLabel | React.ReactNode | "沒有項目" | 空資料夾時顯示的內容 |
renderItem | (node, state) => React.ReactNode | — | 自訂單列渲染 |
className | string | — | 附加到最外層容器的 class |
columnClassName | string | — | 套用到每個欄容器的 class |
MillerNode
| 欄位 | 型別 | 說明 |
|---|---|---|
id | string | 唯一識別碼;用於路徑定位、選取高亮與去重 |
label | React.ReactNode | 顯示標籤;同時用於麵包屑 |
children | MillerNode[] | 子項;有子項者為可展開資料夾,點選推入一欄 |
icon | React.ReactNode | 標籤前的圖示 |
meta | React.ReactNode | 標籤右側的附屬內容(數量、日期等) |
disabled | boolean | 停用此項(不可選取、不可聚焦) |
細節
- 每選一個資料夾就把其子項作為新的一欄,從右側以彈簧(spring)推入;退出的欄則淡出並向右移出,
AnimatePresence的popLayout模式讓退場欄不佔版位、避免回退時抖動 - 以
ResizeObserver量測視口寬度;當所有欄的總寬超出視口,整排以彈簧靠右對齊橫移(橫移量 = 內容總寬 − 視口寬),最新展開的欄永遠可見 - 麵包屑即時反映目前路徑並高亮當前層;點任一層以
path.slice截斷回退,根層標籤則收合到最上層 - 受控 / 非受控雙模式:傳
path走受控,否則以defaultPath內部自管;兩種模式下onPathChange都會觸發 - 選到葉節點(無
children)時只更新高亮與麵包屑、不再推欄,並額外觸發onLeafSelect
可及性
- 使用者系統開啟「減少動態效果」時,推欄、橫移與退場的彈簧過渡全部歸零,直接切換到定位;版面與功能不受影響
- 每一欄為
role="listbox"(aria-orientation="vertical"),每列為role="option"並以aria-selected標記目前選取項;停用項標記aria-disabled - 鍵盤操作:
↑/↓在同欄移動焦點,Home/End跳至首末,→展開資料夾並把焦點移入新欄,←回到上一層的已選項,Enter/Space選取 - 採 roving tabindex:每欄僅有目前選取項(或首項)可用
Tab進入,其餘由方向鍵操作;展開後焦點自動移入新欄(preventScroll) - 麵包屑為原生
<nav><ol>結構,各層是可聚焦按鈕,當前層標記aria-current="page"