apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
TreeView
Variant↳ other

Data Display

TreeView

Hierarchical tree primitive with the full W3C ARIA TreeView keyboard pattern. Two equivalent APIs (data-driven and compound

TreeView

<TreeView /> is the canonical hierarchical-data primitive — file explorers, category pickers, JSON inspectors, taxonomy navigation. It ships the full W3C ARIA TreeView keyboard pattern, a roving tabindex, type-to-search, controlled / uncontrolled expansion and selection, and an async loadChildren slot for lazy file systems.

Overview — file explorer with three expanded levels

Loading preview…
Overview.tsx

Mental model

Tree is a <ul role="tree"> of <li role="treeitem"> rows; one row holds focus at a time and the keyboard pattern dictates how focus moves between them. Selection, expansion, and async loading are bolted on top of the focus pattern — never around it.

APIs

Two equivalent APIs sharing the same DOM. Pick whichever fits the data you have.

Data-driven (default)

tsx
<TreeView
  ariaLabel="Project files"
  data={[
    { id: 'src', label: 'src', children: [
      { id: 'src/Button.tsx', label: 'Button.tsx' },
    ]},
    { id: 'README.md', label: 'README.md' },
  ]}
  defaultExpanded={['src']}
  onSelect={(id) => openFile(id)}
/>
<TreeView
  ariaLabel="Project files"
  data={[
    { id: 'src', label: 'src', children: [
      { id: 'src/Button.tsx', label: 'Button.tsx' },
    ]},
    { id: 'README.md', label: 'README.md' },
  ]}
  defaultExpanded={['src']}
  onSelect={(id) => openFile(id)}
/>

Compound

tsx
<TreeView ariaLabel="Mailbox">
  <TreeView.Node id="inbox" label="Inbox" defaultExpanded>
    <TreeView.Node id="inbox/today" label="Today" />
    <TreeView.Node id="inbox/week"  label="This week" />
  </TreeView.Node>
  <TreeView.Node id="sent" label="Sent" />
</TreeView>
<TreeView ariaLabel="Mailbox">
  <TreeView.Node id="inbox" label="Inbox" defaultExpanded>
    <TreeView.Node id="inbox/today" label="Today" />
    <TreeView.Node id="inbox/week"  label="This week" />
  </TreeView.Node>
  <TreeView.Node id="sent" label="Sent" />
</TreeView>

Internally the compound form is walked once and converted to the same TreeNodeData[] shape — keyboard navigation, async loading, and selection math run from a single projection.


Selection

selectionModeBehaviour
'none' (rare)Tree acts as a navigation tree only — no aria-selected, no callbacks.
'single' (default)Click / Enter / Space sets selected. ArrowDown moves focus without changing selection (per W3C).
'multiple'Click toggles selection (with Ctrl/Cmd for additive). Ctrl/Cmd+A selects all visible nodes. Pair with showCheckboxes to render a leading <Checkbox>.
tsx
<TreeView
  selectionMode="multiple"
  showCheckboxes
  selected={selectedIds}
  onSelectedChange={(next) => setSelectedIds(next as string[])}
/>
<TreeView
  selectionMode="multiple"
  showCheckboxes
  selected={selectedIds}
  onSelectedChange={(next) => setSelectedIds(next as string[])}
/>

Use isRowSelectable (via per-node selectable: false) for "group headers" that focus but never select.


Expansion

Clicking anywhere on a branch row — the chevron or the label — toggles expansion, mirroring the affordance you'd expect from VS Code's explorer or Finder's sidebar. In selectable modes the same click also updates selected, so a single tap both opens a folder and marks it as the active node.

Uncontrolled:

tsx
<TreeView data={data} defaultExpanded={['src', 'src/components']} />
<TreeView data={data} defaultExpanded={['src', 'src/components']} />

Controlled:

tsx
<TreeView data={data} expanded={expanded} onExpandedChange={setExpanded} />
<TreeView data={data} expanded={expanded} onExpandedChange={setExpanded} />

Per-node uncontrolled (compound form):

tsx
<TreeView.Node id="x" label="X" defaultExpanded>…</TreeView.Node>
<TreeView.Node id="x" label="X" defaultExpanded>…</TreeView.Node>

Async children

Provide loadChildren(node) and mark async-only branches with hasChildren: true. The tree dedupes in-flight loads per id and replaces the loading sentinel with the resolved children on success, or an error chip with a retry button on failure.

tsx
<TreeView
  data={[{ id: 'root', label: 'Cloud Drive', hasChildren: true }]}
  loadChildren={(node) => fetch(`/api/tree?parent=${node.id}`).then((r) => r.json())}
/>
<TreeView
  data={[{ id: 'root', label: 'Cloud Drive', hasChildren: true }]}
  loadChildren={(node) => fetch(`/api/tree?parent=${node.id}`).then((r) => r.json())}
/>

The error path keeps the parent focused — consumers can retry with the inline button or re-trigger via loadChildrenIfNeeded(node) from a custom UI.


Custom rendering

renderNode(node, state) lets you build any per-row visual you want — icons, badges, metadata chips, status dots. Selection, focus, expansion math are all handled by the component; the state object is read-only context.

tsx
<TreeView
  data={files}
  renderNode={(node, state) => (
    <span className="flex items-center gap-2">
      <span>{state.expanded ? '📂' : '📁'} {node.label}</span>
      {node.meta?.modified && <Badge>·</Badge>}
    </span>
  )}
/>
<TreeView
  data={files}
  renderNode={(node, state) => (
    <span className="flex items-center gap-2">
      <span>{state.expanded ? '📂' : '📁'} {node.label}</span>
      {node.meta?.modified && <Badge>·</Badge>}
    </span>
  )}
/>

When renderNode is not provided, Tree renders the default chrome (chevron + optional icon + label). Provide defaultIcon / expandedIcon / leafIcon for a quick visual override without writing a full renderer.


Keyboard (W3C TreeView pattern)

KeyAction
ArrowDown / ArrowUpMove focus to next / previous visible row. Skips disabled nodes.
ArrowRight (LTR)Collapsed branch → expand. Expanded branch → focus first child. Leaf → no-op.
ArrowLeft (LTR)Expanded branch → collapse. Collapsed branch / leaf → focus parent.
Home / EndFocus first / last visible row.
Enter / SpaceSingle-mode → set selection. Multi-mode → toggle selection. Branch in selectionMode="none" → expand/collapse. Enter on a branch in single-mode also toggles expansion.
*Expand every sibling branch under the focused node's parent.
Letters / digitsType-to-search aggregator (500ms buffer). Jumps focus to the next visible row whose string label starts with the buffer. Non-string labels are skipped.
Ctrl/Cmd+AMulti-mode only — select all visible, selectable rows.

The pattern automatically swaps ArrowLeft/ArrowRight semantics in RTL via the browser's native direction-aware keyboard event flow.


Accessibility

  • Root: <ul role="tree" aria-label={ariaLabel} aria-multiselectable={selectionMode === 'multiple'}>.
  • Row: <li role="treeitem" aria-level={depth} aria-posinset aria-setsize aria-expanded aria-selected aria-disabled aria-busy tabIndex={focused ? 0 : -1}>.
  • Children group: <ul role="group"> nested inside its parent <li>.
  • One roving tabindex — the tree occupies a single tab stop.
  • Loading + error sentinels use role="none" so screen-reader navigation skips them; the visible label still announces via the live region around the sentinel.
  • Decorative chevrons and icons are aria-hidden="true"; the accessible name comes from the row's label <span> (or the consumer's renderNode).
  • Type-to-search keystrokes are captured and preventDefault()-ed only when they match a searchable character — text editing inside renderNode controls is unaffected.

Zero jest-axe violations across default, sortable, single + multi selection, async loading, disabled, custom render, compound, and RTL modes.


Internationalization

<I18nProvider> doesn't exist yet, so TreeView accepts a translations prop that overlays English defaults:

tsx
<TreeView translations={{ expand: 'הרחב', collapse: 'כווץ', loading: 'טוען…' }} />
<TreeView translations={{ expand: 'הרחב', collapse: 'כווץ', loading: 'טוען…' }} />
KeyDefaultUsed for
expand"Expand"reserved — chevron aria-label when surfaced
collapse"Collapse"reserved
loading"Loading…"async sentinel text + Spinner accessible name
loadError"Failed to load"async-error label
retry"Retry"retry button label

When <I18nProvider> lands, the same keys will be consumed from context with the prop acting as an override.


RTL

  • Indent uses paddingInlineStart so depth flips automatically.
  • ArrowLeft / ArrowRight semantics swap in RTL via the browser's direction handling.
  • showLines connectors use border-inline-start so they sit on the logical start edge.

Performance

  • flattenTree is memoized on data + expanded identity; the projection rebuilds only when the visible row list could change.
  • Selection / expansion lookups are O(1) (Set).
  • Type-to-search is O(visible) per keystroke — no precomputed index needed at typical tree sizes.

Anti-patterns

  • Don't render arbitrary interactive controls (buttons, links) inside a row — that defeats the single-tab-stop pattern. Use onSelect / onSelectedChange for actions.
  • Don't drive the tree with a Set or Map in selected / expanded. The component normalises both into Sets internally; the public contract is strings and string arrays.
  • Don't pass selectionMode="multiple" without getRowId-equivalent stable ids on every node — selection state lives on those ids.
  • Don't fork the visual chrome by overriding treeItemRecipe classes alone — write a renderNode instead. Recipe overrides apply to every row uniformly, but renderNode branches per node state.

More examples

AsyncLoad

Loading preview…
AsyncLoad.tsx

BasicFileExplorer

Loading preview…
BasicFileExplorer.tsx

CategoryPicker

Loading preview…
CategoryPicker.tsx

Compound

Loading preview…
Compound.tsx

ControlledExpansion

Loading preview…
ControlledExpansion.tsx

CustomRender

Loading preview…
CustomRender.tsx

DisabledNodes

Loading preview…
DisabledNodes.tsx

JsonInspector

Loading preview…
JsonInspector.tsx

KeyboardDemo

Loading preview…
KeyboardDemo.tsx

MultiSelectCheckboxes

Loading preview…
MultiSelectCheckboxes.tsx

NonSelectableHeaders

Loading preview…
NonSelectableHeaders.tsx

SingleSelect

Loading preview…
SingleSelect.tsx

Sizes

Loading preview…
Sizes.tsx

Translations

Loading preview…
Translations.tsx

Props

PropTypeDefaultDescription
ariaLabelstring—Accessible name for the tree. **Required** unless `aria-labelledby` is provided. The W3C pattern requires every `role="tree"` to have an accessible name.
childrenReactNode—Compound nodes — wins over `data` when both are provided.
dataTreeNodeData[]—Hierarchical data array. Omit when using the compound `<TreeView.Node>` API.
defaultExpandedstring[]—Uncontrolled initial expansion.
defaultIconReactNode—Default icon for collapsed branches.
defaultSelectedstring | string[]—Uncontrolled initial selection.
expandedstring[]—Controlled expansion.
expandedIconReactNode—Icon for expanded branches.
indentnumber20Indentation in pixels per depth level.
leafIconReactNode—Icon for leaf nodes.
loadChildren(node: TreeNodeData) => Promise<TreeNodeData[]>—Lazy children resolver. Called once per branch the first time it expands.
onExpandedChange(expanded: string[]) => void—Fires when the expansion set changes.
onSelect(id: string) => void—Single-select sugar — receives the new active id (or `''` when cleared).
onSelectedChange(selected: string | string[]) => void—Fires when the selection set changes. Receives the canonical shape for the current mode.
refRef<HTMLUListElement>—Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
renderNodeTreeNodeRenderer—Override the per-row visual layout while keeping all keyboard / a11y behavior.
selectedstring | string[]—Controlled selection. `string` for `single`, `string[]` for `multiple`.
selectionModeenum'single'—
showCheckboxesboolean—When true and selectionMode is `multiple`, render a `<Checkbox>` adornment per node.
showLinesbooleanfalseShow VS-Code style sibling connector lines on the leading edge.
sizeenum'md'—
sxSx——
translationsPartial<TreeViewTranslations>—Override individual translation keys without supplying the whole bundle.