Tree
An arbitrary-depth hierarchical tree — recursive expand/collapse nodes with roving-tabindex keyboard navigation (WAI-ARIA Tree View), for file explorers, folder/nav sidebars, and threaded or org structures. Terse or composed items, selection (single / multiple with range + ⌘A), tri-state checkbox cascade, filter-to-reveal with highlighting, scroll-to-node, async lazy children, and inline rename.
@garn/ui/treeDefault
A file tree — terse nodes with folder/file icons, a disabled node, default-expanded branches.
- src
- components
- button.tsx
- tree.tsx
- index.ts
- README.md
- assets
Selection
Single-select navigation — one active node (aria-selected), selection following focus.
- src
- components
- button.tsx
- tree.tsx
- index.ts
- README.md
Checkable
A checkbox tree — tri-state parent↔child cascade with indeterminate parents.
- assets
- images
- logo.svg
- hero.png
- icon.png
- fonts
Sizes
The size ladder — sm · md · lg row geometry.
sm
- Documents
- notes.md
- report.pdf
md
- Documents
- notes.md
- report.pdf
lg
- Documents
- notes.md
- report.pdf
Guides Actions
Indent guide lines, trailing badges, and per-node hover actions.
- Engineering12
- Frontend
- Backend
- Design4
- Don Norman
Filter
Filter-to-reveal — Tree.Search hides non-matches, auto-expands ancestors, highlights.
- src
- README.md
Async
Async lazy children — a loadable node shows a spinner then loads its subtree on expand.
- my-bucket
- local.txt
Multi Select
Multi-select — ⌘/Ctrl+A, Shift+Arrow range, PageUp/Down.
- src
- components
- button.tsx
- tree.tsx
- input.tsx
- index.ts
- README.md
Rename
Inline rename — F2 / double-click swaps the label for an editable field.
- My files
- Untitled.md
- photo.png
Skeleton
A loading preset — indented chevron/icon/label placeholders.
Empty
An empty state via Tree.Empty composing <Empty>.
- No files yet
Files you add to this project will show up here.
Reorderable
Drag-reparent — a grip handle per node; drag to move nodes between parents (3-way drop zones, cycle-safe), with onMove reshaping nested data.
- src
- components
- button.tsx
- tree.tsx
- index.ts
- README.md
- logo.svg
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add tree- Registry
tree- Source
- tree.tsx
- Deps
@radix-ui/react-slotlucide-react- Registry deps
utilsuse-announceuse-controllable-key-setsortablecheckboxinputspinner
Import
import { Tree } from "@garn/ui/tree";Props
Generated from the component's TypeScript types.
checkedStrategy- Description
- What `onCheckedChange` emits: everything checked, only topmost parents, or only leaves. Internal state is always leaves; this shapes the callback.
- Type
"all" | "leaves" | "parents"- Default
all
selectionMode- Description
- Selection model — sets `aria-selected` on the active node(s).
- Type
"multiple" | "none" | "single"- Default
none
size- Description
- Row height / type ladder.
- Type
"lg" | "md" | "sm"- Default
md
cascade- Description
- Parent↔child check conduction. `false` = independent checks (checkStrictly).
- Type
boolean
checkable- Description
- Render a checkbox per node (independent of `selectionMode`).
- Type
boolean
checkedKeys- Description
- Controlled checked keys (parents included when fully checked).
- Type
string[]
defaultCheckedKeys- Description
- Uncontrolled initial checks.
- Type
string[]
defaultExpandedKeys- Description
- Uncontrolled initial expansion.
- Type
string[]
defaultFilterValue- Description
- Uncontrolled initial filter query (drive it with `<Tree.Search>`).
- Type
string
defaultSelectedKeys- Description
- Uncontrolled initial selection.
- Type
string[]
density- Description
- Local density override; omit to inherit the ambient `[data-density]`.
- Type
Density
disabledKeys- Description
- Keys that cannot be selected/checked/focused.
- Type
string[]
expandedKeys- Description
- Controlled expanded keys.
- Type
string[]
filterValue- Description
- Controlled filter query — hides non-matches, auto-reveals matching branches.
- Type
string
guides- Description
- Draw vertical indent guide lines down each level.
- Type
boolean
label- Description
- Convenience accessible name for the tree.
- Type
React.ReactNode
onCheckedChange- Description
- Called with the next checked set (leaves + fully-checked parents).
- Type
(keys: string[]) => void
onExpandedChange- Description
- Called with the next expanded set whenever it changes.
- Type
(keys: string[]) => void
onFilterChange- Description
- Called with the next filter query whenever it changes.
- Type
(query: string) => void
onMove- Description
- Fires when a node is dropped onto a new parent/position. You move your data: remove `value` from its old spot and insert it into `parent`'s children at `index` (a null `parent` means the tree root). Cycles are pre-filtered out.
- Type
(value: string, target: { parent: string | null; index: number; }) => void
onSelectionChange- Description
- Called with the next selection whenever it changes.
- Type
(keys: string[]) => void
reorderable- Description
- Enable drag-reparent (needs a `<Tree.DragHandle>` in each node's row).
- Type
boolean
revealKey- Description
- Reveal a node: expand its ancestors, then focus + scroll it into view.
- Type
string
selectedKeys- Description
- Controlled selected keys (each `Tree.Item` needs a `value`).
- Type
string[]
selectionFollowsFocus- Description
- In `single` mode, move selection as focus moves (default false).
- Type
boolean
Plus 279 inherited native <ul> attributes.
Tree.DragHandle
label- Description
- Accessible name for the icon-only handle.
- Type
string
Plus 290 inherited native HTML attributes.
Tree.Item
actions- Description
- Terse mode: trailing focusable controls (kept above the row).
- Type
React.ReactNode
disabled- Description
- Inert, non-selectable, non-checkable, skipped by nav + cascade.
- Type
boolean
editable- Description
- Allow inline rename (F2 / double-click the label). Requires a string `label`.
- Type
boolean
icon- Description
- Terse mode: the leading icon (open/closed-aware via context).
- Type
React.ReactNode
label- Description
- Terse mode: the node label (also the primary click / activation target).
- Type
React.ReactNode
loadable- Description
- Marks a parent whose children load on demand — shows a chevron with no items yet.
- Type
boolean
loading- Description
- Children are loading — swaps the chevron for a spinner.
- Type
boolean
onActivate- Description
- Fired when the row is activated (Enter / click).
- Type
(event: React.SyntheticEvent) => void
onRename- Description
- Called with the committed label when an inline rename is confirmed.
- Type
(nextLabel: string) => void
trailing- Description
- Terse mode: inert trailing metadata (count / badge).
- Type
React.ReactNode
valueRequired- Description
- Stable key — required to participate in expansion / selection / checks.
- Type
string
Plus 277 inherited native HTML attributes.
Tree.Label
asChild- Description
- Render as the child element (e.g. a link or heading), merging props.
- Type
boolean
Plus 280 inherited native HTML attributes.
Tree.Skeleton
icon- Description
- Show a leading icon placeholder.
- Type
boolean
rows- Description
- How many placeholder rows to render (default 5).
- Type
number
size- Description
- No description.
- Type
TreeSize
Plus 280 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="tree"][data-slot="tree-label-heading"][data-slot="tree-row"][data-slot="tree-toggle"][data-slot="tree-check"][data-slot="tree-match"][data-slot="tree-rename"][data-slot="tree-drop-indicator"]| Property | Token | Tier |
|---|---|---|
rowGap | --garn-gap-inline | semantic · air |
rowHeight | --garn-control-h-md | component · identity |
rowRadius | --garn-radius | semantic |
motion | --garn-motion-fast | semantic · air |
Private instance vars (never externalize): --tree-indent
Overview
Tree is the hierarchical collection — recursive, same-shape nodes that expand and collapse to arbitrary depth, navigated with a full WAI-ARIA Tree View keyboard model. Reach for it for file/folder explorers, nested navigation sidebars, and threaded or org/category structures. Tree.Item is one node (terse label/icon/trailing/actions, or composed slots); nested items become its children. It scales from a read-only outline to selection (single active node, or multi-select with range + ⌘A), to a checkbox tree with tri-state cascade — plus filter-to-reveal, scroll-to-node, async lazy loading, and inline rename. For single-level expandable rows in a grid use Table; for peer rows with no hierarchy use List; for a few stacked open/close sections use Accordion.
Guidelines
Give every node a stable value — it keys expansion, selection, and checks. Drive state with the controlled/uncontrolled pairs (expandedKeys/defaultExpandedKeys, selectedKeys, checkedKeys) and their on…Change callbacks.
Choose the interaction model deliberately. selectionMode is the active-node model (single = one highlighted node, multiple = range/⌘A); checkable is the orthogonal checkbox model (containment cascade by default, cascade={false} for independent flags). They can coexist as two independent state sets.
Lean on the built-in depth. Tree.Search (or filterValue) filters and auto-reveals; revealKey scrolls a deep node into view; loadable/loading load a subtree on first expand; editable/onRename rename in place; reorderable + onMove + a Tree.DragHandle in each row add drag-reparent (pointer + keyboard, 3-way drop zones, cycle-safe). Virtualization and a data-driven items API stay recipes with a windowing library, never built-in props.
Keep it accessible. Name the tree; don't strip the roving model or the focus ring; convey state with the checkbox/aria, not colour alone.
Best practices
- Give the tree an accessible name (label / aria-label) and every node a stable
value. - Drive expansion/selection/checks with the controlled or default key sets and their on…Change callbacks.
- Use
selectionModefor the active-node model and the orthogonalcheckablefor a containment checkbox tree (they can coexist). - Add
Tree.Search(orfilterValue) for find-in-tree, andloadable/loadingto lazy-load a subtree on first expand. - Enable
reorderable+onMovewith aTree.DragHandlein each row to let users drag nodes to a new parent/position (apply the move to your data inonMove). - Show
Tree.Skeletonwhile the tree loads and an <Empty> (via Tree.Empty) when it's cleared.
- Don't wrap a whole <Tree.Item> in another component (e.g. a ContextMenu trigger) — it hides the node from the tree model; compose inside the item instead.
- Don't put
asChilddirectly on Tree.Label under a Radix trigger — it clobbers the label's data-slot (breaking type-ahead/highlight); wrap a plain element within the label. - Don't reach for Tree for a flat list, a data grid, or a couple of collapsible sections — use List / Table / Accordion.
- Don't rely on colour alone for checkbox state — the tri-state Checkbox carries aria-checked (incl. mixed).
Content guidelines
- Lead the node with its name; keep trailing metadata short (a count / badge) and put actions in Tree.Actions.
- Use open/closed-aware icons (folder vs folder-open) via Tree.Icon's data-expanded for scannability.
Accessibility
- Role
tree- Focus
- A single roving tabindex on the treeitems (active node = 0, others = -1); Arrow keys move focus and the focus ring is drawn on the row from the focused <li>. The chevron and per-node checkbox are tabindex=-1 (operated by keyboard-at-the-treeitem or pointer) so the tree stays one tab stop; Tree.Actions rove with the active node.
Enforceable contracts
Give the Tree an accessible name via `label`, `aria-label`, or `aria-labelledby`.
when a Tree has no `label`, `aria-label`, or `aria-labelledby`
Keyboard
| SpaceEnter | On a Tree.DragHandle: pick the node up (reorderable), then drop it at the chosen position. |
| ArrowUpArrowDown | While dragging a node: move the drop target to the previous / next visible node. |
| ArrowRight | While dragging: nest the node inside the current target. |
| ArrowLeft | While dragging: outdent — move the target up to the parent level. |
| Escape | Cancel an in-progress node drag and restore. |
- • The tree is a <ul role=tree> of <li role=treeitem>; a node's children are wrapped in a <ul role=group>. Each treeitem carries aria-level, aria-setsize, and aria-posinset computed from its position; parents carry aria-expanded (leaves do not).
- • Roving tabindex — exactly ONE node is tabbable (tabindex=0) at a time (the last-focused node, else the first enabled visible node); all others are -1. Tab enters/leaves the whole tree as a single stop.
- • Keyboard (APG Tree View): Up/Down move through visible nodes; Right expands a collapsed parent then descends; Left collapses an open parent then ascends to the parent; Home/End jump to the first/last visible node; type-ahead focuses the next node whose label matches; `*` expands every sibling parent at the current level.
- • Selection: `selectionMode="single"` sets aria-selected on one node (optionally following focus); `selectionMode="multiple"` sets aria-multiselectable on the tree and aria-selected per node, with Space/click toggle, Shift+Arrow / Shift+Click range extension, and ⌘/Ctrl+A select-all-visible.
- • `checkable` renders a real Checkbox per node (tabindex=-1, operated by Space or pointer). Under `cascade` (default), checking a parent conducts to all enabled descendants and a parent shows aria-checked=mixed when only some descendants are checked; conduction stops at disabled nodes. `cascade={false}` (checkStrictly) makes checks independent.
- • The chevron toggle is decorative (aria-hidden, tabindex=-1) — expansion is announced via the treeitem's aria-expanded and driven by the keyboard, so the toggle never becomes a second tab stop. `Tree.Actions` keep their own tab stops within the active node.
- • Filter-to-reveal hides non-matching nodes, force-expands the ancestors of matches, and highlights the matched substring; roving/keyboard operate over the filtered-visible set. Inline rename swaps the label for an input whose data-slot keeps the tree keyboard hands-off (Enter/blur commit, Escape cancels). Give the tree an accessible name via `label`, `aria-label`, or `aria-labelledby` (dev-warns if missing).
- • Drag-reparent (`reorderable`) drags via a real <button> `Tree.DragHandle` (accessible name required), so the node keeps its role=treeitem — a role=button on the row would break the tree model. Fully keyboard-operable: Space/Enter on the handle picks the node up; arrows move the drop target through the visible nodes; Right nests it inside the target, Left outdents it; Space/Enter drops and Escape cancels. Each pick-up/move/drop is announced to a polite live region; a node can never be dropped into its own subtree (cycle-safe).
- Status
- Stable
- Version
v0.3.0- Introduced
v0.2.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.