Pathfinderby Commenda
Components

Tree

Recursive folders and files with keyboard navigation, remote child states, and validated moves.

Import Tree and its types from @commenda/pathfinder/tree.

API reference

PropTypeBehavior
labelRequiredstringRequired tree name
nodesRequiredreadonly TreeNode[]Globally unique IDs, acyclic immutable data
selectionConfigObjectView fields · 2 shapesControlled value/onValueChange or defaultValue; single selection
expansionConfigObjectView fields · 2 shapesControlled/default expanded branch IDs
onLoadChildren(id: string) => voidApp request when an unloaded branch opens or error retries
menuConfig( node: TreeNode ) => DropdownMenuApp actions appended after built-in tree actions
onCreateFolder( parentId: string ) => voidEnables New folder control and menu action
moveConfigTreeMoveConfigView fieldsEnables pointer dragging, Move menu and keyboard move dialog
emptyContentReactNodeDefaults to No items
classNamestringRoot wrapper classes
selectionConfigObject fields

Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.

Option 1

FieldTypeRequirement
valuestring | nullRequired
onValueChange(value: string | null) => voidRequired

Do not supply: defaultValue.

Option 2

FieldTypeRequirement
defaultValuestring | nullOptional
onValueChange((value: string | null) => void)Optional

Do not supply: value.

expansionConfigObject fields

Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.

Option 1

FieldTypeRequirement
valuereadonly string[]Required
onValueChange(value: readonly string[]) => voidRequired

Do not supply: defaultValue.

Option 2

FieldTypeRequirement
defaultValuereadonly string[]Optional
onValueChange((value: readonly string[]) => void)Optional

Do not supply: value.

moveConfigObject fields
FieldTypeRequirement
canMove(request: TreeMove) => booleanRequired
onMove(request: TreeMove) => void | Promise<void>Required

Every node has id, label, optional icon, count, and disabled. A leaf has kind: 'leaf'. A branch has kind: 'branch' and explicit children:

TS
// Undefined does not mean an empty folder.
type TreeChildren =
  | { status: 'unloaded' | 'loading' }
  | { status: 'loaded'; nodes: readonly TreeNode[] }
  | { status: 'error'; message?: string };

Entity roots use flags. Branches default to the exact open/closed Folder artwork; pass a matching FileType icon for files. The component preserves labels for assistive technology and exposes the full truncated label on hover. Duplicate/empty IDs render a clear error instead of creating unstable navigation.

Loading and retry

The example deliberately fails its first request. Retry succeeds. The app owns fetching, cancellation and replacing nodes; mark loading while pending, distinguish loaded-empty from unloaded, and supply readable failures. Known ancestor paths are revealed for a newly selected node; callers must provide expansion IDs for ancestors that have not been loaded yet.

Moving items

Drag a row, choose Move from its menu, or press Ctrl/⌘ + Shift + M.

TreeMove is { sourceId, targetId, position: 'before' | 'after' | 'inside' }. Self and descendant targets are always rejected; inside requires a branch. canMove adds application permission and business rules and must be pure. onMove returns void or a promise; the application updates nodes only after accepting the change. No optimistic data mutation occurs inside Tree. Pending requests cannot duplicate; rejection keeps the dialog open and announces an error for retry.

Pointer drop zones are the top quarter (before), middle half (inside), and bottom quarter (after). Invalid targets do not accept drops. Touch or keyboard users choose Move from the menu and select a destination/placement. Ctrl/⌘ + Shift + M opens the same move dialog for the focused item; Shift + F10 or the context-menu key opens its actions. The example includes an API-failure toggle.

Keyboard

Focus is separate from selection. Up/Down move among visible nodes; Right expands or enters children, Left collapses or returns to parent (mirrored in RTL); Home/End jump to edges; typing finds matching names. Enter/Space selects. Chevron activation expands without selecting. Row actions remain discoverable on focus/touch, and the menu includes New folder/Retry/Move alternatives. Removing the focused row recovers focus to a surviving tree item.

Scope deep folders

Use Focus this folder to show a subtree and a return path. Tree accepts arbitrary depth but caps visual indentation after four levels to keep labels usable in narrow panels; aria-level and hierarchy remain accurate. Prefer scoped browsing for deeper structures and load large branches incrementally. The stress fixture covers 1,000 rendered leaves; it is not a virtualization or unbounded-scale guarantee. Read the hierarchy rules.

Design rules

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

Tree

  • TREE-001: Rows 28px, radius 6, horizontal inset 8 and gap 6, indent 16 per visual level; 12px chevron in a reserved 16px slot even on leaves. Exact Folder artwork, flags at entity roots, file-type icons for files, Label/13, optional count; New folder/More on hover, focus and touch. Hover pearl200, selected pearl300 without a weight change, drop target blue100/blue600.
  • TREE-002: Semantic nested treeitem/group hierarchy and one roving row tab stop. Focus is independent from selection; arrows, Home/End, typeahead and Enter/Space work. RTL mirrors expand/collapse arrows. Full accessible labels preserve truncated text. Loaded selected ancestors reveal; app supplies unknown remote ancestors. Removal recovers focus.
  • TREE-003: Explicit unloaded/loading/loaded/error states. Empty folder is not an unknown folder. Loading uses a skeleton/status; errors have Retry and a keyboard menu alternative. API requests, cancellation and immutable data replacement are app-owned.
  • TREE-004: Every move uses the same validation: no self, no descendant, inside only a branch, then application canMove. Drag before/inside/after, menu Move and Ctrl/⌘+Shift+M share one commit path. The app updates data after success; duplicate pending moves are blocked; failure is announced and retryable. Permissions must also be enforced by the application/API.

Scale and scope

Do not silently remove deep nodes. Keep semantic depth accurate; visual indentation stops growing after four levels. Compose a focused subtree with a return/breadcrumb path for deeper work. Load large folders incrementally; this component does not promise virtualization. Keep worst-case fixtures in Storybook, not enterprise. Test long non-Latin names, huge counts, empty/single/large branches, disabled nodes, data replacement, RTL, keyboard and pointer moves, and async rejection.

Reviewed accessibility and geometry adjustments

Tree prose describes a 4px inset, while the reviewed item 818:274 uses 8px horizontal padding and a 6px gap. The implementation follows the component and the full tree 820:211, including its root spacer and 2px row gaps.

Explicit differences and boundaries

ReferenceCoverage
Cell master 430:13537Text/description/icon/badge, people/groups/email, entities, files, tags, boolean, progress, copy/link, numbers/units/currency/trends, loading and totals compose TableCell, TableNumberCell and TableRowActions. Tree parent/child/grandchild behavior belongs to the recursive DataGrid examples. Hover and selection share the row tint in both densities.

On this page