Skip to content

Tree

Hierarchical list view with expand/collapse, single or multi-select, and full WAI-ARIA keyboard support.

  • stable
  • since v0.1.0
  • 3.1 kB
  • data-display
  • tree
  • hierarchy

Implements the WAI-ARIA tree pattern — roving tabindex, ArrowUp/Down/Left/Right navigation, Home/End jumps, and aria-expanded / aria-selected / aria-level on every node.

Preview
Open
tsx

Pass defaultExpandedIds to open a set of branches on first paint without controlling state.

Preview
Open
tsx

Pass selectionMode="single" (plus optional defaultSelectedIds) for a picker-style tree.

Preview
Open
tsx

Pass selectionMode="multiple" for checkbox-style multi-select. Ctrl/Cmd-click toggles individual ids; aria-multiselectable flips to true on the root.

Preview
Open
tsx

Pass a render function as the children prop to fully customise each row — receive the node plus depth, expanded, selected, focused, and disabled flags.

Preview
Open
tsx

Tree accepts an array of TreeNode-shaped items by default — { id, label?, children?, disabled?, icon? }. If your data already matches, drop it in directly.

const items: TreeNode[] = [
{ id: 'a', label: 'A', children: [{ id: 'a/1', label: 'A1' }] },
];
<Tree items={items} aria-label="Files" />

Your data doesn’t match? Use accessor props

Section titled “Your data doesn’t match? Use accessor props”

When the consumer’s data has different field names (uuid, name, subItems, …), point at them with getId / getLabel / getChildren / getDisabled instead of rebuilding the array.

interface ApiCollectionItem {
uuid: string;
name: string;
items?: ApiCollectionItem[];
locked?: boolean;
}
<Tree<ApiCollectionItem>
items={apiData}
getId={(n) => n.uuid}
getLabel={(n) => n.name}
getChildren={(n) => n.items}
getDisabled={(n) => n.locked}
aria-label="API collections"
/>

The render prop’s item argument is fully typed as ApiCollectionItem, so you can read your own fields directly without casts.

Prefer a one-shot transform? Use mapToTreeNodes

Section titled “Prefer a one-shot transform? Use mapToTreeNodes”

If you’d rather normalise once at the data boundary and reuse the result elsewhere, the same accessors are accepted by the mapToTreeNodes helper:

import { Tree, mapToTreeNodes } from '@arshad-shah/cynosure-react';
const items = mapToTreeNodes(apiData, {
getId: (n) => n.uuid,
getLabel: (n) => n.name,
getChildren: (n) => n.items,
getDisabled: (n) => n.locked,
});
<Tree items={items} aria-label="API collections" />

Pick whichever fits the call site — both paths produce identical behaviour.

PropTypeDefaultDescription
items*
T[]
Root nodes. Each may carry its own `children` for nesting.
getId
((item: T) => string)
Override how the tree reads a node's stable id. Defaults to `(item) => item.id`. Use this when your data uses a different field (e.g. `uuid`, `slug`).
getLabel
((item: T) => ReactNode)
Override how the tree reads a node's default label. Defaults to `(item) => item.label`. Only consulted when no `children` render prop is supplied.
getChildren
((item: T) => T[])
Override how the tree reads a node's nested children. Defaults to `(item) => item.children`. Map your own field (e.g. `subItems`, `nodes`) without rebuilding the data into the {@link TreeNode} shape.
getDisabled
((item: T) => boolean)
Override how the tree reads a node's disabled flag. Defaults to `(item) => item.disabled`.
expandedIds
string[]
Controlled set of expanded node ids. Omit with `defaultExpandedIds` for uncontrolled.
defaultExpandedIds
string[]
[]
Initial expanded node ids when uncontrolled.
onExpandedChange
((ids: string[]) => void)
Fires whenever the expanded set changes (either mode).
selectedIds
string[]
Controlled set of selected node ids.
defaultSelectedIds
string[]
[]
Initial selected node ids when uncontrolled.
onSelectionChange
((ids: string[]) => void)
Fires whenever the selection changes.
selectionMode
"none"|"single"|"multiple"
"none"
Selection behaviour: `none` disables selection; `single` keeps a single selected id; `multiple` toggles ids on ctrl/meta-click and exposes `aria-multiselectable`.
children
TreeRenderItem<T>
Render each item. Receives the node + contextual flags.
aria-label
string
Accessible label for the `role="tree"` host.
aria-labelledby
string
id of a labelling element — alternative to `aria-label`.
  • Implements the WAI-ARIA tree pattern: the root is role="tree", each row is role="treeitem", and nested branches are role="group".
  • Every node exposes aria-expanded (when it has children), aria-selected (when selection is enabled), aria-level, and aria-disabled.
  • Keyboard: ArrowUp / ArrowDown move focus; ArrowRight opens a branch or moves to the first child; ArrowLeft closes a branch or moves to the parent; Home / End jump to the first / last visible row; Enter / Space toggle and activate; * expands every sibling at the current level.
  • Roving tabIndex keeps the tree a single tab stop within the surrounding form / page.
  • Provide an aria-label (or aria-labelledby) so the tree’s purpose is announced.
  • Use defaultExpandedIds for uncontrolled expansion; pair expandedIds + onExpandedChange to control state externally.
  • Switch selectionMode between "single" and "multiple" for picker-style trees.
  • Pass a children render prop to layer in custom icons, badges, or trailing actions.
  • Use the exported treeCollectIds helper to implement an “expand all” toggle without recursion at the call site.