Skip to content

Navigation

TreeView

A hierarchy with roving focus, expansion state and selection.

Examples

Selection, and what it reports

One tree, one selection. The callback carries the node as well as the id, so a caller never has to search its own data to find out what was clicked.

Markets
Equities12
Rates7
Crypto4
Spot2
Perpetuals2
Archive

Selected id

rates

Click a row, or tab into the tree once and walk it with the arrow keys — the selection follows the focus by default.

Selection does not follow focus

With `selectionFollowsFocus={false}` the arrows browse and only Enter or Space commits — the shape a picker inside a panel wants, where arrowing past a row must not change the pane beside it.

Datasets
Research
Reports28
Datasets9
Archive

Expansion the caller owns

`expandedIds` makes the open set the caller's, which is what a saved view or a remembered position needs. `treeBranchIds` is the helper for an expand-all control.

1 branches open

Markets
Equities12
Rates7
Archive

Long labels, and nodes that are not available

A long label truncates rather than pushing its metadata out of the row. A `disabled` node stays visible and announced — the arrow keys step over it — because a branch the reader is told about and cannot see is worse than one they can see and cannot open.

Portfolios4
Momentum and trend following across global futureslive
Carry12
FX carry8
Rates carry4
Commodity carry—
Archive 2019

Usage

import { TreeView } from "@luc/ui";
<TreeView
  nodes={tree}
  label="Workspace"
  defaultExpandedIds={["markets"]}
  defaultSelectedId="rates"
/>

`nodes` is the hierarchy, `expandedIds`/`selectedId` make either side controlled; `label` is the tree's accessible name and the pattern requires one.

API

Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.

Three symbols: the component and the two helpers a caller needs to drive it from outside — `findTreeNode`, to turn a selected id back into its node, and `treeBranchIds`, for an expand-all control of your own.

Exported symbols
ExportKindSignature
findTreeNodeexport(nodes: readonly TreeNode[], id: string) => TreeNode | undefined
treeBranchIdsexport(nodes: readonly TreeNode[]) => string[]
TreeView10 documented props({ nodes, expandedIds, defaultExpandedIds, onExpandedChange, selectedId, defaultSelectedId, onSelect, selectionFollowsFocus, label, indent, className, ...props }: TreeViewProps) => import("@types/rea…

TreeView props

Type: TreeViewProps

Values the compiler reads from the declaration.

TreeView props
PropTypeDefaultNotes
defaultExpandedIds?readonly string[] | undefined—
defaultSelectedId?string | null | undefined—
expandedIds?readonly string[] | undefined—Controlled set of open ids.
indent?number | undefined—Indentation per level, in pixels. Default 16.
label?string | undefined—Accessible name for the tree. Required — a tree with no name is a puzzle.
nodesreadonly TreeNode[]—
onExpandedChange?((ids: string[]) => void) | undefined—
onSelect?((id: string, node: TreeNode) => void) | undefined—
selectedId?string | null | undefined—Controlled selection. `null` means "nothing is selected".
selectionFollowsFocus?boolean | undefined—Move the selection with the arrow keys, not only the focus. Default `true`.

Guidelines

Do

  • Name the tree with `label`: it is the only thing that tells one tree from another, and an unnamed `role="tree"` is a puzzle to a screen reader.
  • Keep `selectionFollowsFocus` on unless selecting *is* an edit — it is what makes the arrow keys feel like a list rather than a corridor.
  • Use `disabled` for a branch that exists but is unavailable: the row stays visible and announced, and the keyboard steps over it.

Don't

  • Do not build a menu out of a tree: two levels of navigation is a sidebar, and a chooser over 200 flat options is a `Combobox`.
  • Do not expand the whole hierarchy by default — `treeBranchIds` is there for an explicit expand-all control, and a tree that opens itself loses the reader's place on every visit.
  • Do not nest more than a few levels: depth without search is a filing cabinet, and a reader cannot see where they are in one.

Accessibility

The tree follows the ARIA `tree`/`treeitem` pattern rather than approximating it, and the consequence a reader has to know is that the whole control is **one Tab stop**: `tabindex` roves with the focused row, so Tab leaves the tree rather than walking out of it row by row. `aria-level`, `aria-setsize` and `aria-posinset` state the shape, so a screen reader announces "3 of 8, level 2" instead of an indentation nobody can see; a collapsed branch leaves the keyboard order entirely; and a `disabled` node is still rendered and announced, but the arrow keys step over it.

↑ / ↓

Move along the rows that are visible.

→

Open a closed branch, then step into its first child.

←

Close an open branch, then step out to its parent.

Home / End

Jump to the first or the last visible row.

Enter / Space

Select the focused row.

*

Open every closed sibling of the focused row.

Luc UI v1.0TreeViewdark theme