File Tree

Hierarchical tree view for files, folders, and any nested navigation. Implements the WAI-ARIA tree pattern with role='tree' / 'treeitem' / 'group', aria-level, aria-expanded, aria-selected, and full keyboard navigation (Up/Down/Left/Right/Home/End/Enter/Space).

Installation

pnpm
pnpm dlx @hex-core/cli add file-tree

Usage

tsx
import { File Tree } from "@/components/ui/file-tree"

Basic file tree

Uncontrolled expanded set; selected state controlled

tsx
import { useState } from "react";
import { FileTree } from "@/components/ui/file-tree";

const nodes = [
  {
    id: "src",
    name: "src",
    children: [
      { id: "src/index.tsx", name: "index.tsx" },
      {
        id: "src/components",
        name: "components",
        children: [
          { id: "src/components/Button.tsx", name: "Button.tsx" },
          { id: "src/components/Input.tsx", name: "Input.tsx" },
        ],
      },
    ],
  },
  { id: "package.json", name: "package.json" },
];

export function Example() {
  const [selected, setSelected] = useState<string>();
  return (
    <FileTree
      aria-label="Project files"
      nodes={nodes}
      defaultExpanded={["src"]}
      selected={selected}
      onSelect={setSelected}
    />
  );
}

API Reference

PropTypeDefaultDescription
nodesrequired
objectTree of { id, name, children?, icon?, disabled? }. Presence of `children` (even an empty array) marks the node as a folder.
defaultExpanded
objectUncontrolled — initial expanded ids (string[]).
expanded
objectControlled expanded ids (string[]). Pair with onExpandedChange.
onExpandedChange
functionFired with the new expanded ids: (ids: string[]) => void
selected
stringControlled selected node id.
onSelect
functionFired when the user activates a node via click, Enter, or Space: (id: string) => void
aria-labelrequired
stringRequired accessible name for the tree (e.g. 'File explorer', 'Settings sections').

AI Guidance

When to use

Use for hierarchical navigation: file/folder explorers, settings sections, org charts, taxonomy browsers. Renders a real ARIA tree with full keyboard support, so it works for sighted, keyboard, and screen-reader users.

When not to use

Don't use for flat lists (use ScrollArea + a list). Don't use for navigation menus (use NavigationMenu). Don't use for very deep trees (>5 levels) without virtualization — every node is rendered. Don't use for selecting multiple files concurrently — multi-select tree UX is a different beast; ship a separate component when you need it.

Common mistakes

  • Mixing controlled `expanded` with `defaultExpanded` — pass exactly one
  • Using non-stable node ids (e.g. array index) — collapsing/expanding shifts state
  • Marking a leaf with `children: []` instead of omitting `children` — empty array still flags it as a folder, so the chevron shows
  • Forgetting aria-label — the tree gets no accessible name and screen readers announce just 'tree'
  • Calling onSelect to navigate without de-bouncing arrow-key focus changes — focus moves on arrows but does NOT call onSelect; only Enter/Space/click selects, so navigation should hang off onSelect, not focused state
  • Expecting row-click to toggle expand — per WAI-ARIA tree pattern the row click only selects; toggling is the chevron button (or ArrowRight/Left, or Enter/Space when the row is focused). Common surprise after coming from VS Code-style trees
  • Passing `selected` pointing at a node inside a collapsed branch — the tree falls back to the first visible node for tab focus, so the consumer can't rely on tabIndex to land on the selected target until it's revealed via expanded

Accessibility

Root: role='tree' with aria-label. Each node: role='treeitem' with aria-level, aria-expanded (folders only), aria-selected, tabIndex=0 only on the active visible node (roving tabindex). Children container: role='group'. Click semantics: row click selects only; the chevron is a separate decorative button that toggles. Keyboard: ArrowDown/Up move through visible non-disabled nodes (disabled nodes are skipped); ArrowRight expands a closed folder or moves to first child; ArrowLeft collapses an open folder or moves to parent; Home/End jump to first/last visible; Enter/Space activate (toggle on folders, select on all).

Token budget: 2000