Tree View
Browse and manage nested data with selection, lazy loading and inline renaming.
import { TreeView, TreeViewContextActions } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a tree view for a hierarchy people browse and act on: files in a project, pages in a site, folders to sync, categories and their items. It's a WAI-ARIA tree with one Tab stop and the full keyboard model. For two levels of links in an app's navigation, use a Sidebar. For sections of content that open and close, use an Accordion.
import { TreeView, type TreeNode } from '@youj/react'
const files: TreeNode[] = [
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/main.tsx', name: 'main.tsx' },
],
},
{ id: 'package.json', name: 'package.json' },
]
export function Files() {
return <TreeView aria-label="Files" data={files} defaultExpanded={['src']} onAction={(node) => openFile(node.id)} />
}A node with children is a branch, and one without is a leaf. Folders get icons that open with them and files get icons by type; set icons={false} for a plain tree, or give a node its own icon. meta puts a status letter or a count at the end of the row. It's decorative, so say the same thing in description, which is read after the name.
Selection and open branches can each be controlled (selected, expanded) or left to the tree (defaultSelected, defaultExpanded).
Description
A file explorer: a project tree with a detail pane, rename, a context menu and packages that load on first open.
In the demo, a click selects and a double click or Enter opens a file through onAction. With onRename, F2 renames the focused node in place: Enter keeps the new name and Escape drops it. contextMenu gives every node a menu, on right-click or Shift+F10, and hands you a rename action for it. node_modules is lazy: it loads its children the first time it opens.
A folder picker for syncing, with checkboxes that show a mixed state and a count of selected files.
selectionMode="multiple" turns the tree into a checkbox tree. Checking a folder checks everything loaded inside it, and a folder shows mixed when only some of its contents are chosen. Disabled nodes can't be checked themselves.
selected then holds every checked id, folders included, so count the leaves when you report a total.
Two branches that load their children on first open; the flaky one fails once and shows a retry row.
Give a node hasChildren and no children, and pass loadChildren. The first time the branch opens, the tree shows a loading row and calls it. Put the children into data and resolve. If the promise rejects, a retry row takes the place of the children; Enter on it tries again. In this preview flaky fails once.
A tree of 100 folders with 100 files each, all open, windowed to the rows in view.
virtual windows the rows, for trees of thousands of items: only the rows in view are in the page, and the one the keyboard lands on scrolls into view. Give the tree a height.
The virtual tree is a flat list of rows that carry their level, position and set size, so branches open instantly rather than animating their height.
Code
tree-view-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsdata, expanded, defaultExpanded… (14)
Props
| Prop | Type | |
|---|---|---|
dataRequired | TreeNode[] | – |
expanded | string[]Open branches. | – |
defaultExpanded | string[]default [] | [] |
onExpandedChange | (ids: string[]) => void | – |
selectionMode | 'single' | 'multiple'
default 'single' | 'single' |
selected | string[] | – |
defaultSelected | string[]default [] | [] |
onSelectedChange | (ids: string[]) => void | – |
onAction | (node: TreeNode) => voidEnter or double-click on a leaf: open the file. | – |
loadChildren | (node: TreeNode) => Promise<unknown>Loads a lazy branch's children. Resolve once | – |
onRename | (node: TreeNode, name: string) => voidEnables F2 / inline rename. Called with the trimmed new name when it changed. | – |
contextMenu | (node: TreeNode, actions: TreeViewContextActions) => ReactNodeContext-menu items for a node (ContextMenuItem and friends). | – |
icons | booleanFile-explorer icons: folders that open, files by type. default true | true |
virtual | booleanWindow the rows for large trees. Give the tree a height. default false | false |
Other props pass through: extends ComponentProps<'ul'>. | ||
A node (TreeNode) has an id and a name, and optionally children, hasChildren for a lazy branch, an icon, meta, a description and disabled. TreeFileIcon and TreeFolderIcon are the tree's icons, for use beside it, as the editor tabs in a file explorer would.
MotionBranch opens, branch closes, chevron… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Branch opensheight to auto and opacity (Collapse); closing mid-open reverses from where it is | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
Branch closesheight on base, opacity out on fast | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Chevronturns a quarter as the branch opens | no token | ||
| Folderits front panel tips open with the branch | no token | ||
| Row hover and selectionbackground | duration.instantease.standard80ms · cubic-bezier(0.2, 0, 0, 1) | 80mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
Branches open and close instantly, and the chevron and folder switch state without turning.
Reduced motion
Branches appear and disappear at once, with no height animation, and the chevron and folder show their new state without turning or tipping. Everything else, including the loading row and the announcements, is the same.
AccessibilityKeyboard, ARIA, focus
Accessibility
- It's a
treeoftreeitems, named byaria-label, witharia-expandedon branches andaria-level,aria-posinsetandaria-setsizeon every item. - One Tab stop: the focused item carries
tabIndex={0}and the rest-1, so Tab moves past the tree in one step. - Single selection uses
aria-selected. The checkbox tree usesaria-checked,mixedincluded. Disabled items arearia-disabled. - A loading branch is
aria-busy, and loads are announced: "Loading node_modules", then "node_modules loaded" or "Couldn't load node_modules". Renames are announced too.
| Key | Does |
|---|---|
| ↑ ↓ | Move between visible items. |
| → | Opens a closed branch, or moves into an open one. Flipped under right-to-left text. |
| ← | Closes an open branch, or moves out to the parent. |
| Home / End | The first or last visible item. |
| * | Opens every sibling branch. |
| Typing | Jumps to the next item whose name starts with the letters typed. |
| Enter | Opens a file (onAction) or toggles a folder. Retries a failed load. |
| Space | Selects the item, or checks it in a checkbox tree. |
| F2 | Renames it, with onRename. |
| Shift+F10 | Opens its context menu, with contextMenu. |