Skip to main contentSkip to docs navigation

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.

Documents
Report.pdf
Notes.txt
README.md
JSX
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.

Documents
Report.pdf
Archive
2024.zip
README.md
JSX
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.

Documents
Report.pdf
JSX
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.

Documents
Report.pdf
Notes.txt
README.md
Selected: notes
JSX
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.

Documents
Report.pdf
Notes.txt
Archive
2024.zip
README.md
Selected: report, 2024
JSX
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.

Documents
Report.pdf
Notes.txt
README.md
Opened: nothing yet
JSX
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.

src
components
Tree.tsx
TreeItem.tsx
index.ts
package.json
JSX
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:

JSX
<Tree aria-label="Files" items={files} renderEmptyState={() => 'No files yet.'}>
  {renderFile}
</Tree>

Accessibility

  • The tree is a treegrid with aria-multiselectable when the selection is multiple, and each visible item a row with aria-level, aria-posinset, aria-setsize, aria-expanded when it has child items, and aria-selected when 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 hasChildItems flag for children still loading, but as of react-aria-components 1.21 it doesn't reach the row's aria-expanded and arrow keys, so the component doesn't offer it.
  • No links. An item takes no href: a row is a div with role="row", which can't be an <a>, and this library renders every href as one. Navigate from onAction instead.
  • No drag and drop, sections or virtualization. React Aria's tree supports all three; none is wired here yet.

API

Tree

PropTypeDefault
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.
childrenReactNode | ((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

PropTypeDefault
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.
labelReactNode—
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.