Skip to content
ilinxa/pro-ui

File Tree

alphav0.1.3

VS Code-style file tree — format-aware icons, full CRUD, drag-and-drop, lazy children, and multi-select.

Category: NavigationUpdated: 2026-08-11Created: 2026-05-10Author: ilinxa

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

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/file-tree

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/file-tree-fixtures

Preview

my-app30

Demo source

demo.tsxtsx

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 — onMove only 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 updated nodes — you must apply the move yourself.
  • onLoadChildren rejection shows an inline error row + retry button under the folder. Throw a real Error with a useful message.
  • Cut / copy / paste are not in v0.1.0 — they land with folder-manager via a shared clipboard. Use drag-to-move and right-click delete in the meantime.
  • showHidden defaults to false; nodes whose name starts with . are filtered. Override with isHidden for 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

Tags

treenavigationfilesystemexplorerhierarchyfilefoldervscode

Dependencies

shadcn primitives: alert-dialog, button, context-menu, tooltip
npm peer deps: @tanstack/react-virtual@^3.13.24, lucide-react@^1.11.0