Tree

A hierarchy the reader can expand, select, check and drag, with a virtual list for the large ones.

Accessibility

Tree comes with its own roles, states and keyboard behavior. What you add is a name for the tree, clear row text and enough contrast.

What Tree renders

The tree is built from <div> elements with ARIA roles. Which element carries which role decides where your labels go.
  • The scrolling list carries role="tree". It is the only element with a tab stop, so focus and the tree's name both go there.
  • Each row is a treeitem with aria-expanded (not on a leaf), aria-selected (when selectable and enabled), aria-checked (when checkable and enabled) and aria-disabled.
  • The current row is marked with aria-activedescendant on the list. Focus never moves to a row.

Keyboard

The tree takes one tab stop, and the keys below work inside it before your own onKeyDown runs.
  • Up and Down move through the visible rows and wrap at both ends.
  • Right expands a closed branch, or moves to its first child when it is open. Left collapses an open branch, or moves to the parent when it is closed.
  • Home and End jump to the first and last visible rows.
  • Enter expands or collapses any row that can expand, and only checks or selects a row that cannot. On a folder, Enter opens it rather than selecting it.
  • Space checks the row on a checkable tree and selects it otherwise.
  • The arrow keys stop on disabled rows too. They are announced but cannot be acted on.
  • Dragging has no keyboard route. Offer a move command in the row as well.
disabled on the whole tree also removes its tab stop, so keyboard users cannot reach it to hear what it says. focusable={false} removes the tab stop without the disabled look. Prefer disabling single nodes.

Naming the tree and its rows

The tree has no name of its own, so a reader who hears “Invoices, tree item” cannot tell which tree it is.
  • aria-label or aria-labelledby on Tree lands on the role="tree" element. Point aria-labelledby at the panel's heading where there is one.
  • An aria-label on a treeData node reaches its row. Use it when the visible text is an abbreviation or a bare file name.
  • A row shows a native tooltip only when its title is a string. With titleRender there is none, so put that text in what you render.
  • A row's name is its text. An icon-only row has no name, and a count or badge in the title is read as part of it, in order.

The checkbox column

The checkbox is a <span> with role="checkbox", not an input, and it never takes focus. Space on the active row toggles it.
  • It is named by its row, so the checkbox and the row read as one.
  • A half-checked parent announces as partially checked. That is the only sign that a closed branch holds a choice, so do not style the mixed state away.
  • checkStrictly removes the mixed state, so the tree no longer says anything about a partly chosen branch.
  • disableCheckbox disables the box alone and leaves the row usable.
  • A custom node passed as checkable renders inside the checkbox. Put nothing focusable in it.

Focus and the virtual list

  • With height set, rows out of view are not in the DOM, so find in page and test queries miss them.
  • Moving the active row scrolls it into view, which keeps arrow navigation working. itemScrollOffset keeps the active row off the edge.
  • Focus stays on the tree, so put nothing focusable inside a row. A button in titleRender takes a tab stop inside the tree, where the arrow keys no longer reach it. Open row actions from one control outside the tree instead.
  • The tree does not announce when rows appear or disappear. When the count matters, like a filter that empties the tree, say so in a live region beside it.

Contrast and motion

  • Hover and selected are both light fills with the text color unchanged, so selection rests on one quiet fill. Check it against the tree's own background. Where selection matters, use DirectoryTree, whose selected row is the primary color.
  • The showLine guides use the border color, which a theme change can fade. Keep them visible, since depth has only the indent and the lines to show it.
  • Expanding and collapsing animate, and the theme's motion token turns that off.
  • The chevron's rotation is a CSS transition. Put anything you animate on top of it behind motion-safe:.