File Tree
alphav0.1.3VS Code-style file tree — format-aware icons, full CRUD, drag-and-drop, lazy children, and multi-select.
Context
Use anywhere a hierarchical-node array needs an interactive tree — code editors, document workspaces, asset libraries, schema browsers, low-code builders, or as the sidebar inside a dual-pane Finder layout. Controlled-data; consumer owns the `nodes` array; component fires object-shape callbacks on every operation.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/file-treeAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/file-tree-fixturesPreview
my-app30
Demo source
Usage
When to use
FileTree is a vertical, expand-collapse tree for hierarchical content — file system, project structure, schema namespace, asset library. Reach for it whenever your sidebar (or picker dialog, or schema browser) needs the VS Code shape: chevrons, format-aware icons, keyboard nav, optional CRUD. Pair it with folder-manager as the dual-pane Finder layout.
Data shape
The component is fully controlled — consumer owns the nodes array, mutations happen via callbacks, consumer updates state. children has three semantically distinct values: undefined = not yet loaded (triggers onLoadChildren), [] = known-empty, FsNode[] = pre-loaded.
type FsNode = {
id: string; // stable across renders
name: string; // displayed label
type: "file" | "folder";
parentId?: string | null;
children?: FsNode[]; // undefined | [] | FsNode[]
ext?: string; // explicit extension (else derived from name)
size?: number;
modifiedAt?: string;
icon?: ReactNode; // pre-rendered override
meta?: Record<string, unknown>;
}Lazy loading
Provide onLoadChildrenfor folders whose children aren't pre-fetched. The hook sets loadingFolderIds while the promise pends and shows the inline spinner; when it resolves, you splice the new children into nodes (we export mergeLoadedChildren() to do the immutable splice).
import { mergeLoadedChildren } from "./file-tree"
<FileTree
nodes={nodes}
onLoadChildren={async ({ nodeId }) => {
const kids = await fs.list(nodeId);
setNodes((prev) => mergeLoadedChildren(prev, nodeId, kids));
return kids;
}}
/>Selection + keyboard
- Single by default;
selectionMode="multi"for Cmd/Ctrl+click toggle and Shift+click range. - ↑ / ↓ moves focus among visible rows; → / ← expands/collapses or moves into/out of a folder.
- Enter opens a file (
onOpen) or toggles a folder; Space toggles selection; F2 renames; Delete deletes (with confirm); Cmd/Ctrl+A selects all visible rows; Esc clears selection.
Drag-and-drop
- Within the tree: drag a row onto a folder (drop indicator = ring) or above/below another row (line). Cycle and self-drop are pre-validated —
onMoveonly fires for legal drops. Name-collision is your call (handle it insideonMove). - From the desktop: drop OS files onto the tree to fire
onExternalDrop. Wire your upload flow there.
Custom chrome
Replace the default header wholesale via renderHeader (which gets a typed context with actions and the same flags), or compose subsets using the standalone parts: FileTreeHeader, FileTreeNewFileButton, FileTreeNewFolderButton, FileTreeRefreshButton, FileTreeCollapseAllButton. They read from useFileTree().
Gotchas
onMovefires after structural validation but before you've updatednodes— you must apply the move yourself.onLoadChildrenrejection shows an inline error row + retry button under the folder. Throw a realErrorwith a useful message.- Cut / copy / paste are not in v0.1.0 — they land with
folder-managervia a shared clipboard. Use drag-to-move and right-click delete in the meantime. showHiddendefaults tofalse; nodes whose name starts with.are filtered. Override withisHiddenfor app-specific rules.
Features
- Arbitrary-depth nesting with chevron expand/collapse
- Format-aware Lucide icons (override per-node or via `iconForNode`)
- Controlled or uncontrolled selection + expansion
- Single + multi-select with Cmd/Ctrl+click and Shift+click range
- Right-click menu with default actions + `renderContextMenu` slot
- Inline rename via F2 / double-click + optional `validateRename`
- Drag-and-drop reorder with cycle / self-drop pre-validation
- Drag-from-OS support — `onExternalDrop` fires with files + targetId
- Lazy children loading via `onLoadChildren` + exported `mergeLoadedChildren` helper
- Auto-virtualization at ≥200 visible rows (TanStack Virtual)
- Indent guides + sticky header + sortable + hide-dotfiles
- Built-in delete confirmation dialog (replaceable via slot)
- Standalone header parts for custom chrome composition
- Object-shape callbacks (F-cross-12-correct from day one)
- WCAG 2.1 AA — `role=tree`, `aria-level/setsize/posinset/expanded/selected`, focus-visible