Tree
A tree of expandable items with single or multiple selection, keyboard navigation, and items written by hand or rendered from data.
Overview
Tree shows hierarchical items, such as files in folders, that expand and collapse. It renders a
treegrid of rows built on React Aria's tree, one
row per visible item, each with its level and position for assistive technology. Chassis CSS has no
tree, so the component ships its own styles in this package's stylesheet: import
@chassis-ui/react/style.css once, as the Theming page
describes.
Basic usage
Write the items as TreeItem elements, nested inside one another; label is what a row shows and
id its key, which expandedKeys, selectedKeys and disabledKeys name. defaultExpandedKeys
opens some items at first. An item with child items gets a chevron that expands and collapses it,
and the rows of an expanded item render on the server too, so the page shows them before any
script runs. Name the tree with aria-label, or aria-labelledby pointing at a visible heading.
import { Tree, TreeItem } from '@chassis-ui/react'
export const Example = () => (
<Tree aria-label="Files" defaultExpandedKeys={['documents']}>
<TreeItem id="documents" label="Documents">
<TreeItem id="report" label="Report.pdf" />
<TreeItem id="notes" label="Notes.txt" />
<TreeItem id="archive" label="Archive">
<TreeItem id="2024" label="2024.zip" />
</TreeItem>
</TreeItem>
<TreeItem id="pictures" label="Pictures">
<TreeItem id="holiday" label="Holiday.jpg" />
</TreeItem>
<TreeItem id="readme" label="README.md" />
</Tree>
)Each level indents its rows by --cx-tree-indent, so that a child's chevron starts where its
parent's label does by default. The rows read the same custom properties as .list and .menu for their
padding, colors and states, so a page that re-themes lists and menus gets a matching tree.
Icons
Set icon on an item to draw an icon before its label, hidden from assistive technology since the
label names the row. The chevron is IconProvider's expand icon, replaced for one tree with
expandIcon; it turns to point down when the item is expanded, and points the other way under
right-to-left text.
import { Icon, Tree, TreeItem } from '@chassis-ui/react'
const folder = <Icon name="folder-solid" />
const file = <Icon name="file-outline" />
export const Example = () => (
<Tree aria-label="Files" defaultExpandedKeys={['documents', 'archive']}>
<TreeItem icon={folder} id="documents" label="Documents">
<TreeItem icon={file} id="report" label="Report.pdf" />
<TreeItem icon={folder} id="archive" label="Archive">
<TreeItem icon={file} id="2024" label="2024.zip" />
</TreeItem>
</TreeItem>
<TreeItem icon={folder} id="pictures" label="Pictures">
<TreeItem icon={file} id="holiday" label="Holiday.jpg" />
</TreeItem>
<TreeItem icon={file} id="readme" label="README.md" />
</Tree>
)Expanding
expandedKeys and onExpandedChange make the expansion controlled: the tree renders the items
in expandedKeys open, and asks for a change through onExpandedChange with the whole set of
keys that would be expanded. The Right arrow key expands the focused item and the Left arrow key
collapses it, or moves to its parent when it is already collapsed. In a tree with no selection and
no onAction, a click on an item with child items toggles it, as the chevron does.
import { useState } from 'react'
import { Button, Tree, TreeItem } from '@chassis-ui/react'
import type { Key } from 'react-stately'
const folders = ['documents', 'archive', 'pictures']
export const Example = () => {
const [expandedKeys, setExpandedKeys] = useState<Set<Key>>(new Set(['documents']))
return (
<div className="d-flex flex-column gap-sm">
<div className="d-flex gap-sm">
<Button variant="outline" onClick={() => setExpandedKeys(new Set(folders))}>
Expand all
</Button>
<Button variant="outline" onClick={() => setExpandedKeys(new Set())}>
Collapse all
</Button>
</div>
<Tree aria-label="Files" expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys}>
<TreeItem id="documents" label="Documents">
<TreeItem id="report" label="Report.pdf" />
<TreeItem id="archive" label="Archive">
<TreeItem id="2024" label="2024.zip" />
</TreeItem>
</TreeItem>
<TreeItem id="pictures" label="Pictures">
<TreeItem id="holiday" label="Holiday.jpg" />
</TreeItem>
</Tree>
</div>
)
}Selection
Set selectionMode to "single" or "multiple" to make the items selectable. A click, Space or
Enter selects the focused item; a selected row is marked .active and aria-selected. selectedKeys
and onSelectionChange control the selection, which is a Set of keys or the string "all";
defaultSelectedKeys sets it at first. disabledKeys, or disabled on an item, keeps an item out
of the selection and out of keyboard navigation, or out of the selection alone with
disabledBehavior="selection". disallowEmptySelection keeps the last selected item selected.
import { useState } from 'react'
import { Tree, TreeItem } from '@chassis-ui/react'
import type { Selection } from 'react-stately'
export const Example = () => {
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set(['notes']))
return (
<div className="d-flex flex-column gap-sm">
<Tree
aria-label="Files"
defaultExpandedKeys={['documents']}
disabledKeys={['pictures']}
selectedKeys={selectedKeys}
selectionMode="single"
onSelectionChange={setSelectedKeys}
>
<TreeItem id="documents" label="Documents">
<TreeItem id="report" label="Report.pdf" />
<TreeItem id="notes" label="Notes.txt" />
</TreeItem>
<TreeItem id="pictures" label="Pictures">
<TreeItem id="holiday" label="Holiday.jpg" />
</TreeItem>
<TreeItem id="readme" label="README.md" />
</Tree>
<span>
Selected: {selectedKeys === 'all' ? 'all' : [...selectedKeys].join(', ') || 'none'}
</span>
</div>
)
}Multiple selection
With selectionMode="multiple" every item gets a checkbox, named after the item, and a click on
the row or on its checkbox toggles the item. checkboxes={false} leaves the checkboxes out and
keeps the toggling; checkboxes on a single selection adds them.
import { useState } from 'react'
import { Tree, TreeItem } from '@chassis-ui/react'
import type { Selection } from 'react-stately'
export const Example = () => {
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set(['report', '2024']))
return (
<div className="d-flex flex-column gap-sm">
<Tree
aria-label="Files"
defaultExpandedKeys={['documents', 'archive']}
selectedKeys={selectedKeys}
selectionMode="multiple"
onSelectionChange={setSelectedKeys}
>
<TreeItem id="documents" label="Documents">
<TreeItem id="report" label="Report.pdf" />
<TreeItem id="notes" label="Notes.txt" />
<TreeItem id="archive" label="Archive">
<TreeItem id="2024" label="2024.zip" />
</TreeItem>
</TreeItem>
<TreeItem id="readme" label="README.md" />
</Tree>
<span>
Selected: {selectedKeys === 'all' ? 'all' : [...selectedKeys].join(', ') || 'none'}
</span>
</div>
)
}selectionBehavior="replace" selects as a file manager does: a click selects the clicked item
alone, Ctrl or Cmd adds one, Shift selects a range, and the checkboxes stay out unless checkboxes
asks for them.
Actions
onAction is called with an item's key when the item is activated: with a click or Enter in a tree
that has no selection, and with a double click, or Enter, under selectionBehavior="replace". With
the default toggle behavior and a selection, a click activates an item only while nothing is
selected; once something is, a click toggles the selection, until it is empty again. An item's own
onAction is called alongside the tree's. A tree that navigates does so from onAction, with the
app's router.
import { useState } from 'react'
import { Tree, TreeItem } from '@chassis-ui/react'
import type { Key } from 'react-stately'
export const Example = () => {
const [opened, setOpened] = useState<Key | null>(null)
return (
<div className="d-flex flex-column gap-sm">
<Tree aria-label="Files" defaultExpandedKeys={['documents']} onAction={setOpened}>
<TreeItem id="documents" label="Documents">
<TreeItem id="report" label="Report.pdf" />
<TreeItem id="notes" label="Notes.txt" />
</TreeItem>
<TreeItem id="readme" label="README.md" />
</Tree>
<span>Opened: {opened ?? 'nothing yet'}</span>
</div>
)
}Data-driven items
Pass the data to items and a render function as children, called once per entry with the
entry, as DataGrid's body takes its rows. An item takes items and the same kind of function for
its child items, so one function can render every level by passing itself on. The entry's id
becomes the item's key; give textValue when label is not a string, so typing a letter can find
the item. When the function reads a value from outside the data, list it in dependencies, or a
change to it won't re-render the items.
import { Tree, TreeItem } from '@chassis-ui/react'
interface Entry {
id: string
name: string
entries?: Entry[]
}
const project: Entry[] = [
{
id: 'src',
name: 'src',
entries: [
{
id: 'components',
name: 'components',
entries: [
{ id: 'tree', name: 'Tree.tsx' },
{ id: 'tree-item', name: 'TreeItem.tsx' }
]
},
{ id: 'index', name: 'index.ts' }
]
},
{ id: 'package', name: 'package.json' }
]
// The render function renders one entry, and passes itself on for the entry's own entries.
const renderEntry = (entry: Entry) => (
<TreeItem items={entry.entries} label={entry.name} textValue={entry.name}>
{renderEntry}
</TreeItem>
)
export const Example = () => (
<Tree aria-label="Project" defaultExpandedKeys={['src', 'components']} items={project}>
{renderEntry}
</Tree>
)renderEmptyState fills a tree with no items:
<Tree aria-label="Files" items={files} renderEmptyState={() => 'No files yet.'}>
{renderFile}
</Tree>Accessibility
- The tree is a
treegridwitharia-multiselectablewhen the selection is multiple, and each visible item arowwitharia-level,aria-posinset,aria-setsize,aria-expandedwhen it has child items, andaria-selectedwhen it is selectable. The row is named by the item's text. - The tree is one tab stop: Tab enters it at the focused or first item and leaves it from any item. The Up and Down arrow keys move between the visible items, Home and End to the first and last, and typing a letter moves to the next item whose text starts with it.
- The chevron is a button named "Expand" or "Collapse" after the row, for touch screen readers; it is out of the tab order, since the arrow keys do the same.
- The selection checkbox is named "Select" after the row.
Scope
- No lazy children. Every item's children are in the data or the markup when the tree renders.
React Aria's tree item takes a
hasChildItemsflag for children still loading, but as of react-aria-components 1.21 it doesn't reach the row'saria-expandedand arrow keys, so the component doesn't offer it. - No links. An item takes no
href: a row is adivwithrole="row", which can't be an<a>, and this library renders everyhrefas one. Navigate fromonActioninstead. - No drag and drop, sections or virtualization. React Aria's tree supports all three; none is wired here yet.
API
Tree
| Prop | Type | Default |
|---|---|---|
aria-label? | string | — |
| An accessible name for the tree, when no visible heading names it. | ||
aria-labelledby? | string | — |
| The id of the visible heading that names the tree. | ||
autoFocus? | boolean | "first" | "last" | — |
Focus the tree when it mounts: true or 'first' focuses its first item, 'last' its last. | ||
checkboxes? | boolean | — |
Draw a checkbox in every item, which selects the item. On by default with
selectionMode="multiple" and the 'toggle' selection behavior, off otherwise. | ||
children | ReactNode | ((item: T) => ReactElement<unknown, string | JSXElementConstructor<any>>) | — |
TreeItem elements, or a render function called once per entry of items, returning that
entry's TreeItem. | ||
className? | string | — |
| A string of all className you want applied to the component. | ||
defaultExpandedKeys? | Iterable<Key> | — |
| The keys of the items expanded at first (uncontrolled). | ||
defaultSelectedKeys? | Iterable<Key> | "all" | — |
The keys of the items selected at first (uncontrolled): 'all', or an iterable of keys. | ||
dependencies? | readonly unknown[] | — |
Values that should rebuild the items when using the items/render-function form of
children — e.g. a value from outside items that the render function reads. Without this,
changing that value won't re-render the items, which are only rebuilt when items or
dependencies change by reference. | ||
disabledBehavior? | "all" | "selection" | 'all' |
Whether disabledKeys disables every interaction with an item, or only its selection. | ||
disabledKeys? | Iterable<Key> | — |
| The keys of the items that are disabled. | ||
disallowEmptySelection? | boolean | — |
| Keep at least one item selected: the last selected item can't be deselected. | ||
expandedKeys? | Iterable<Key> | — |
| The keys of the expanded items (controlled). | ||
expandIcon? | IconValue | — |
The chevron of an item with child items, in place of IconProvider's expand icon: an icon
name, or an element. | ||
id? | string | — |
| The id of the tree element. | ||
items? | Iterable<T> | — |
The data to render as items, through the function form of children. | ||
onAction? | ((key: Key) => void) | — |
Called with an item's key when the item is activated: with Enter, or with a click when the
tree has no selection. An item's own onAction is called too. | ||
onExpandedChange? | ((keys: Set<Key>) => void) | — |
| Called with the keys of the expanded items when an item expands or collapses. | ||
onSelectionChange? | ((keys: Selection) => void) | — |
Called with the keys of the selected items ('all', or a Set) when the selection changes. | ||
ref? | Ref<HTMLDivElement> | — |
renderEmptyState? | (() => ReactNode) | — |
| What to render in place of the items when there are none. | ||
selectedKeys? | Iterable<Key> | "all" | — |
The keys of the selected items (controlled): 'all', or an iterable of keys. | ||
selectionBehavior? | "toggle" | "replace" | toggle |
How a click changes a multiple selection: 'toggle' adds or removes the clicked item,
'replace' selects it alone, with Ctrl, Cmd and Shift for more, as a file manager does. | ||
selectionMode? | "none" | "single" | "multiple" | none |
| Whether items can be selected, and how many at a time. | ||
style? | CSSProperties | — |
| Inline styles for the tree element. | ||
TreeItem
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | ((item: T) => ReactElement<unknown, string | JSXElementConstructor<any>>) | — |
The item's child items: TreeItem elements, or a render function called once per entry of
items, returning that entry's TreeItem. | ||
className? | string | — |
| A string of all className you want applied to the item. | ||
dependencies? | readonly unknown[] | — |
Values that should rebuild the child items when using the items/render-function form of
children, as Tree's dependencies. | ||
disabled? | boolean | — |
Toggle the disabled state for the item, as a key in the tree's disabledKeys does. | ||
icon? | ReactNode | — |
| An icon drawn before the label. | ||
id? | Key | — |
The item's key, named by expandedKeys, selectedKeys and disabledKeys. Defaults to the
key of the data entry the item renders, or to the item's position. | ||
items? | Iterable<T> | — |
The data to render as child items, through the function form of children. | ||
label | ReactNode | — |
| The item's content. | ||
onAction? | (() => void) | — |
| Called when the item is activated: with Enter, or with a click when the tree has no selection. | ||
ref? | Ref<HTMLDivElement> | — |
style? | CSSProperties | — |
| Inline styles for the item's row. | ||
textValue? | string | — |
The item's text, which typing a letter in the tree searches. Defaults to label when it is a
string. | ||