App Shells & Panels

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

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/miller-column-browser.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

240
Active edited usageDefault props
<MillerColumnBrowser/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/miller-column-browser.en.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

PropTypeDefaultDescription
itemsMillerNode[]—Root-level items of the hierarchical data
pathstring[]—Controlled mode: the current selection path (array of node ids)
defaultPathstring[][]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
breadcrumbsbooleantrueWhether to show the breadcrumb bar at the top
rootLabelReact.ReactNode"Root"Label shown for the root level in the breadcrumbs
columnWidthnumber240Width of each column (px)
emptyLabelReact.ReactNode"No items"What to show for an empty folder
renderItem(node, state) => React.ReactNode—Custom rendering for a single row
classNamestring—Class attached to the outermost container
columnClassNamestring—Class applied to each column container

MillerNode

FieldTypeDescription
idstringUnique identifier; used for path resolution, selection highlighting, and de-duplication
labelReact.ReactNodeDisplay label; also used in the breadcrumbs
childrenMillerNode[]Child items; a node with children is an expandable folder that pushes a column when selected
iconReact.ReactNodeIcon before the label
metaReact.ReactNodeTrailing content to the right of the label (counts, dates, and so on)
disabledbooleanDisables 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's popLayout mode keeps the exiting column out of the layout so going back does not jitter
  • A ResizeObserver measures 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 path for controlled, otherwise defaultPath is managed internally; onPathChange fires in both modes
  • Selecting a leaf node (no children) only updates the highlight and breadcrumbs without pushing a column, and additionally fires onLeafSelect

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 a role="option" marked with aria-selected for the current selection; disabled items are marked aria-disabled
  • Keyboard: ↑ / ↓ move focus within a column, Home / End jump to the first / last, → expands a folder and moves focus into the new column, ← returns to the selected item one level up, and Enter / Space select
  • 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 marked aria-current="page"