Skip to content

TreeView

Shows items that belong to one another as a set of rows that open and shut. Use it for a folder listing, a navigation sidebar, or any grouped list a reader needs to fold away.

tsx
import { TreeItem, TreeView } from 'neba';

<TreeView label="Project files" lines="folder" defaultExpanded={['src']}>
  <TreeItem value="src" label="src">
    <TreeItem value="index" label="index.ts" />
  </TreeItem>
  <TreeItem value="readme" label="README.md" />
</TreeView>;

Props

TreeView

PropTypeDefaultDescription
variantshared'solid' | 'outline' | 'text''outline'Weight of the surface: filled, hairline, or none
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The row height, the type scale, the sheet's radius, and how far one level is set in
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'A row's horizontal padding only. Never the row height, never the indentation
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
lines'none' | 'simple' | 'folder''simple'How the hierarchy is drawn. `none` is indentation alone, `simple` is one rail per level, and `folder` adds an elbow into every row and stops the rail under a last child
expanded(string | number)[]Which branches are open. Use with onExpandedChange for a controlled tree
defaultExpanded(string | number)[]Which start open
onExpandedChange(expanded: (string | number)[]) => voidFires when a branch opens or shuts
selected(string | number)[]Which rows are chosen. An array even with multiple off: the same shape Accordion's value takes, so turning multiple on does not change the type of the value
defaultSelected(string | number)[]Which start chosen
onSelectedChange(selected: (string | number)[]) => voidFires when the selection changes
multiplebooleanfalseWhether more than one row may be chosen at a time. Also sets aria-multiselectable
disabledbooleanfalseUnavailable. Every row stops answering
labelstringThe name the tree is announced by, as aria-label on the tree
childrenReactNodeThe top-level TreeItems

expanded with onExpandedChange makes the open branches controlled; defaultExpanded makes them uncontrolled. selected and onSelectedChange work the same way for the selection, and both values are arrays of the rows' values.

Every other <ul> attribute passes through to the tree. The shared axes are in prop conventions.

TreeItem

PropTypeDefaultDescription
valuestring | numberIdentifies the row to expanded and selected. One is generated when it is left out, which is fine for a tree nobody drives from code
labelReactNodeThe row's text. Its own prop rather than children, because in a tree the children are the rows underneath it
startIconReactNodeContent before the label: a folder glyph, a file type, a status dot
endIconReactNodeContent after the label: a count, a badge
actionReactNodeA control pinned to the end of the row, outside the pressable area: a row that both opens and holds a menu button has two things to press
hrefstringRenders the row as a link, for a tree that is navigation
onClickMouseEventHandler<HTMLElement>Fires when the row is pressed, before it opens or is chosen. Calling preventDefault stops both
expandablebooleanForces the disclosure arrow onto a row with no children yet: the branch fetched the first time it is opened
disabledbooleanfalseUnavailable. Its branch, if open, keeps working
childrenReactNodeThe TreeItems underneath this one

Every other <li> attribute passes through to the row.

Examples

lines

lines decides how the hierarchy is drawn. none indents and nothing else, simple runs one hairline rail down each level, and folder adds an elbow into every row and stops the rail under the last child of a branch.

variant

The sheet is never filled with colour. Use text inside a Card or a sidebar that already has a surface.

Selecting rows

Pressing a row chooses it, and opens it if it has children. multiple lets more than one row be chosen at a time; without it, choosing a row replaces whatever was chosen before.

The disclosure arrow is a target of its own: it opens the branch without choosing the row.

href

A row with an href renders as a link, which is what a navigation tree is made of. It is not a second tab stop: the tree holds one, and the arrow keys reach the rows.

expandable

A shut branch is not in the DOM, so a tree that fetches its children the first time a row is opened has nothing to render yet. expandable draws the arrow anyway: fetch in onExpandedChange, then render the rows when they arrive.

tsx
<TreeView expanded={expanded} onExpandedChange={load}>
  <TreeItem value="remote" label="Remote" expandable>
    {children.map((child) => (
      <TreeItem key={child.id} value={child.id} label={child.name} />
    ))}
  </TreeItem>
</TreeView>

Accessibility

  • The tree is a tree, every row is a treeitem, and a branch's children are a group.
  • The whole tree is one tab stop. Once inside, ArrowUp and ArrowDown walk the visible rows, ArrowRight opens a shut branch and steps into an open one, ArrowLeft shuts a branch and climbs out of a leaf, Home and End jump to the ends, and Enter chooses the focused row. The arrows never change the selection.
  • ArrowLeft and ArrowRight swap under RTL, so the forward arrow always means "further in".
  • Pass label so the tree has a name; without one, a screen reader announces an unnamed tree.
  • multiple sets aria-multiselectable on the tree.

Released under the MIT License