Miller Column Browser
A Finder-style Miller-column hierarchy browser: every level you descend springs a new column in from the right, the whole row pans smoothly once it overflows, and breadcrumbs highlight the current path in step.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/miller-column-browser.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<MillerColumnBrowser />
Installation
npx shadcn@latest add "https://webberui.com/r/miller-column-browser.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/miller-column-browser.
Usage
Pass in a tree of MillerNodes (items). A node with children is a folder — selecting it pushes a new column in from the right showing its children; a node without children is a leaf, and selecting it only highlights, without expanding further. The breadcrumbs reflect the current path in step, and clicking any level jumps back to it.
import {
MillerColumnBrowser,
type MillerNode,
} from "@/components/ui/miller-column-browser";
const items: MillerNode[] = [
{
id: "design",
label: "Design system",
children: [
{ id: "color", label: "Color", children: [{ id: "neutral", label: "Neutrals" }] },
{ id: "type", label: "Typography" },
],
},
{ id: "docs", label: "Documentation" },
];
<div className="h-[320px]">
<MillerColumnBrowser items={items} rootLabel="Library" />
</div>;Every node is a MillerNode (id / label, with optional children / icon / meta / disabled). children can nest arbitrarily, producing columns of any depth.
Controlled mode
Pass path and onPathChange to enter controlled mode, where the current selection path (an array of node ids from the root to the current node) is fully owned outside. Without them it is uncontrolled, starting from defaultPath and managing state internally.
const [path, setPath] = React.useState<string[]>(["design", "color"]);
<MillerColumnBrowser
items={items}
path={path}
onPathChange={(next) => setPath(next)}
/>;Listening for leaf selection
onLeafSelect only fires when a node with no children is selected, which makes it a good place to load details or trigger navigation once a final item has been picked.
<MillerColumnBrowser
items={items}
onLeafSelect={(node, path) => console.log("selected", node.id, path)}
/>Props
MillerColumnBrowser
| Prop | Type | Default | Description |
|---|---|---|---|
items | MillerNode[] | — | Root-level items of the hierarchical data |
path | string[] | — | Controlled mode: the current selection path (array of node ids) |
defaultPath | string[] | [] | Uncontrolled mode: initial selection path |
onPathChange | (path: string[], node: MillerNode | null) => void | — | Callback when the selection path changes |
onLeafSelect | (node: MillerNode, path: string[]) => void | — | Callback when a leaf node (no children) is selected |
breadcrumbs | boolean | true | Whether to show the breadcrumb bar at the top |
rootLabel | React.ReactNode | "根目錄" | Label shown for the root level in the breadcrumbs |
columnWidth | number | 240 | Width of each column (px) |
emptyLabel | React.ReactNode | "沒有項目" | What to show for an empty folder |
renderItem | (node, state) => React.ReactNode | — | Custom rendering for a single row |
className | string | — | Class attached to the outermost container |
columnClassName | string | — | Class applied to each column container |
MillerNode
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier; used for path resolution, selection highlighting, and de-duplication |
label | React.ReactNode | Display label; also used in the breadcrumbs |
children | MillerNode[] | Child items; a node with children is an expandable folder that pushes a column when selected |
icon | React.ReactNode | Icon before the label |
meta | React.ReactNode | Trailing content to the right of the label (counts, dates, and so on) |
disabled | boolean | Disables this item (not selectable, not focusable) |
How it works
- Selecting a folder turns its children into a new column that springs in from the right; the column being left fades out and moves right, and
AnimatePresence'spopLayoutmode keeps the exiting column out of the layout so going back does not jitter - A
ResizeObservermeasures the viewport width; when the total width of all columns overflows it, the whole row pans with a spring so that it is right-aligned (the pan distance = total content width − viewport width), which keeps the most recently expanded column always visible - The breadcrumbs reflect the current path live and highlight the current level; clicking any level truncates back with
path.slice, and the root label collapses to the topmost position - Controlled and uncontrolled modes: pass
pathfor controlled, otherwisedefaultPathis managed internally;onPathChangefires in both modes - Selecting a leaf node (no
children) only updates the highlight and breadcrumbs without pushing a column, and additionally firesonLeafSelect
Accessibility
- When the user has "reduce motion" enabled at the system level, the spring transitions for pushing, panning, and exiting all drop to zero and positioning becomes instant; layout and functionality are unaffected
- Each column is a
role="listbox"(aria-orientation="vertical") and each row arole="option"marked witharia-selectedfor the current selection; disabled items are markedaria-disabled - Keyboard:
↑/↓move focus within a column,Home/Endjump to the first / last,→expands a folder and moves focus into the new column,←returns to the selected item one level up, andEnter/Spaceselect - Roving tabindex: only the currently selected item (or the first) in each column is reachable with
Tab, with the rest driven by arrow keys; focus moves into the new column automatically after expanding (preventScroll) - The breadcrumbs are a native
<nav><ol>structure whose levels are focusable buttons, with the current level markedaria-current="page"
Notification Triage Panel
A notification center panel grouped by date or source, with unread/all tabs and collapsible groups; on a bulk clear the items sweep out in sequence and the group height springs closed behind them.
Elastic Split Workspace
A multi-column drag-to-split panel workspace whose dividers have spring feedback; panels collapse to a thin strip in one click and the neighbouring panels reflow elastically.