TreeView
When to use
Section titled “When to use”- Files and folders, as in an editor’s explorer
- Choosing from nested categories, or settings grouped in levels
Not for a site’s navigation. A tree is one Tab stop that is driven with the arrow keys, which few people expect of a menu of links. Use nested lists of Links, with an Accordion for sections that open.
Choosing between nested and grouped content:
| Component | Use it for | For example |
|---|---|---|
| TreeView | A hierarchy to move through and select from, any number of levels | Files and folders, nested categories |
| Accordion | Sections of content that open one at a time or together, one level | An FAQ, a settings page’s groups |
| Menu | Actions opened from a button, with submenus for more | “More actions ⋯”, a View menu |
| Tabs | Views of one thing that people switch between | Overview, Activity and Settings of a repo |
- src
- components
- main.ts
- env.d.ts
- docs
- package.json
- README.md
import { useState } from 'react';import { Stack, Text, TreeView } from '@arun-dev/ui';
export default function TreeViewBasics() { // The selection is a list of values, even with one selected: the same shape as `multiple`. const [value, setValue] = useState<string[]>(['src/main.ts']);
return ( <Stack gap="sm" style={{ inlineSize: '100%', maxInlineSize: '20rem' }}> <TreeView.Root aria-label="Project files" value={value} onValueChange={setValue} defaultExpanded={['src']} > {/* Values are paths here: unique in the tree, and useful once selected. */} <TreeView.Item value="src" label="src"> <TreeView.Item value="src/components" label="components"> <TreeView.Item value="src/components/Button.tsx" label="Button.tsx" /> <TreeView.Item value="src/components/Card.tsx" label="Card.tsx" /> </TreeView.Item> <TreeView.Item value="src/main.ts" label="main.ts" /> <TreeView.Item value="src/env.d.ts" label="env.d.ts" disabled /> </TreeView.Item> <TreeView.Item value="docs" label="docs"> <TreeView.Item value="docs/intro.md" label="intro.md" /> <TreeView.Item value="docs/setup.md" label="setup.md" /> </TreeView.Item> <TreeView.Item value="package.json" label="package.json" /> <TreeView.Item value="README.md" label="README.md" /> </TreeView.Root> <Text size="sm" color="secondary" aria-live="polite"> Selected: {value[0] ?? 'nothing'} </Text> </Stack> );}Anatomy
Section titled “Anatomy”import { TreeView } from '@arun-dev/ui';
<TreeView.Root aria-label="Project files" defaultExpanded={['src']}> <TreeView.Item value="src" label="src"> <TreeView.Item value="src/main.ts" label="main.ts" /> </TreeView.Item> <TreeView.Item value="README.md" label="README.md" /></TreeView.Root>;| Part | Renders | Does |
|---|---|---|
Root | <ul role="tree"> | holds the selection and the open Items; name it with aria-label |
Item | <li role="treeitem"> | one node: a row with its label, and its child Items while it’s open |
Nesting Items makes a parent. An Item with child Items opens and closes, with a chevron
that turns. One without is a leaf. The row and the <ul role="group"> around the children are
drawn by the Item, so there are no parts for them.
Values identify Items. Each Item’s value must be unique in the tree. A path, such as
src/main.ts, is unique and useful once selected. The Root’s value lists the selected
Items, and expanded lists the open ones. Each can be controlled or left to the tree.
Keyboard
Section titled “Keyboard”The tree is one Tab stop: the Item last focused, or the selected one, or the first.
| Key | Does |
|---|---|
| ↓ / ↑ | Moves to the next or previous Item shown. Does not wrap. |
| → | Opens a closed parent; on an open one, moves to its first child |
| ← | Closes an open parent; anywhere else, moves to the Item it’s nested in |
| Home / End | Moves to the first or last Item shown |
| Enter | Selects the Item, and opens or closes a parent, as a click does |
| Space | Selects the Item, without opening it |
* | Opens every parent beside the focused Item |
| A letter | Moves to the next Item whose label starts with what you type |
In a right-to-left tree, → and ← swap, so the arrow points toward the children.
Selecting several
Section titled “Selecting several”- Repository
- Read code
- Push code
- Issues
- Read issues
- Open and close issues
- Administration
import { useState } from 'react';import { Stack, Text, TreeView } from '@arun-dev/ui';
export default function TreeViewMultiple() { const [value, setValue] = useState<string[]>(['repo:read', 'issues:write']);
return ( <Stack gap="sm" style={{ inlineSize: '100%', maxInlineSize: '20rem' }}> <TreeView.Root aria-label="Token permissions" multiple value={value} onValueChange={setValue} defaultExpanded={['repo', 'issues']} > {/* Selecting a parent selects that Item only: the tree does not cascade to children. */} <TreeView.Item value="repo" label="Repository"> <TreeView.Item value="repo:read" label="Read code" /> <TreeView.Item value="repo:write" label="Push code" /> </TreeView.Item> <TreeView.Item value="issues" label="Issues"> <TreeView.Item value="issues:read" label="Read issues" /> <TreeView.Item value="issues:write" label="Open and close issues" /> </TreeView.Item> <TreeView.Item value="admin" label="Administration"> <TreeView.Item value="admin:billing" label="Billing" /> <TreeView.Item value="admin:members" label="Members" /> </TreeView.Item> </TreeView.Root> <Text size="sm" color="secondary" aria-live="polite"> {value.length} selected{value.length > 0 ? `: ${value.join(', ')}` : ''} </Text> </Stack> );}With multiple, Space, Enter or a click adds an Item to the selection or removes it.
Shift with ↓ or ↑ moves and toggles the next Item, and Ctrl or ⌘ with A selects every Item
shown. Selecting a parent selects that Item alone: the tree does not cascade to the children
or show a parent as partly selected. If the children should follow, set them in
onValueChange.
Disabled Items
Section titled “Disabled Items”A disabled Item is drawn faded, can’t be focused, selected, opened or closed, and the arrow
keys skip it. A screen reader still finds it when reading the whole tree, announced as unavailable.
Labels
Section titled “Labels”label is what the row shows and what a screen reader reads, so an icon goes in it, marked
aria-hidden when it adds nothing to the words. Typing moves to the next Item whose label
starts with what’s typed, using the text found in label. When label is a component that
renders its own text, give the Item a textValue for typing to match.
Playground
Section titled “Playground”- src
- App.tsx
- main.tsx
- public
- index.html
// value: []
// expanded: ["src"]
<TreeView.Root
aria-label="Files"
value={value}
onValueChange={setValue}
expanded={expanded}
onExpandedChange={setExpanded}
>
<TreeView.Item value="src" label="src">
<TreeView.Item value="src/App.tsx" label="App.tsx" />
<TreeView.Item value="src/main.tsx" label="main.tsx" />
</TreeView.Item>
<TreeView.Item value="public" label="public">
<TreeView.Item value="public/favicon.svg" label="favicon.svg" />
</TreeView.Item>
<TreeView.Item value="index.html" label="index.html" />
</TreeView.Root>Accessibility
Section titled “Accessibility”- Name the tree with
aria-label, such as “Project files”. - Each Item is named by its label alone, not by the children nested in it.
aria-expanded,aria-selectedandaria-levelare set for you, andaria-multiselectablewithmultiple.- Selection is shown by a tint and a bar on the row’s leading edge, so it doesn’t depend on colour alone, and the focused row has its own outline.
TreeView.Root
Section titled “TreeView.Root”| Prop | Type | Default | Description |
|---|---|---|---|
value | string[] | Controlled: the selected Items' values. Provide onValueChange alongside it. Never pass undefined — the mode is fixed at mount. | |
defaultValue | string[] | [] | Initial selection when uncontrolled. Read once, at mount. |
onValueChange | (value: string[]) => void | Called with the whole selection whenever it changes. | |
multiple | boolean | false | Lets more than one Item be selected: Space, Enter or a click toggles one, Shift with an arrow key toggles the next, and Ctrl or ⌘ with A selects every Item shown. Without it, selecting an Item replaces the selection. |
expanded | string[] | Controlled: the open parent Items' values. Provide onExpandedChange alongside it. Never pass undefined — the mode is fixed at mount. | |
defaultExpanded | string[] | [] | Initially open parent Items when uncontrolled. Read once, at mount. |
onExpandedChange | (expanded: string[]) => void | Called with every open Item whenever one opens or closes. | |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
TreeView.Item
Section titled “TreeView.Item”| Prop | Type | Default | Description |
|---|---|---|---|
value | string | Identifies the Item in the Root's value and expanded. Unique in the tree. Required. | |
label | ReactNode | What the row shows, and the Item's accessible name. Icons go in it too. Required. | |
textValue | string | What typeahead matches, when the text in label is not it — or when label is a component that renders its own text. | |
disabled | boolean | false | Cannot be focused, selected, opened or closed. Skipped by the arrow keys. |
children | ReactNode | Child Items. With any, the Item is a parent: it opens and closes, and renders them only while open. | |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
- An Item’s children render only while it’s open, so a large tree costs only what’s shown.
To load children when a folder opens, set them from
onExpandedChange. Until they arrive, a placeholder child Item, such as “Loading…”, keeps the folder a parent. - Restyle with the
--tree-view-*tokens, such as--tree-view-indentor--tree-view-item-bg-selected.