WebberUI

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.jsonfiles[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>;

每個節點是一個 MillerNodeid / label,選配 children / icon / meta / disabled)。children 可任意巢狀,形成任意深度的欄。

受控模式

傳入 pathonPathChange 即進入受控模式,由外部完全掌握目前選取路徑(由根到當前的節點 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型別預設值說明
itemsMillerNode[]階層資料的根層項目
pathstring[]受控模式:目前選取路徑(節點 id 陣列)
defaultPathstring[][]非受控模式:初始選取路徑
onPathChange(path: string[], node: MillerNode | null) => void選取路徑變更時的回呼
onLeafSelect(node: MillerNode, path: string[]) => void選到葉節點(無子項)時的回呼
breadcrumbsbooleantrue是否顯示頂端麵包屑列
rootLabelReact.ReactNode"根目錄"根層在麵包屑顯示的標籤
columnWidthnumber240每欄寬度(px)
emptyLabelReact.ReactNode"沒有項目"空資料夾時顯示的內容
renderItem(node, state) => React.ReactNode自訂單列渲染
classNamestring附加到最外層容器的 class
columnClassNamestring套用到每個欄容器的 class

MillerNode

欄位型別說明
idstring唯一識別碼;用於路徑定位、選取高亮與去重
labelReact.ReactNode顯示標籤;同時用於麵包屑
childrenMillerNode[]子項;有子項者為可展開資料夾,點選推入一欄
iconReact.ReactNode標籤前的圖示
metaReact.ReactNode標籤右側的附屬內容(數量、日期等)
disabledboolean停用此項(不可選取、不可聚焦)

細節

  • 每選一個資料夾就把其子項作為新的一欄,從右側以彈簧(spring)推入;退出的欄則淡出並向右移出,AnimatePresencepopLayout 模式讓退場欄不佔版位、避免回退時抖動
  • 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"

On this page