Displays a hierarchy of files and folders with curved branch connectors, an accent line that draws to the selection, and keyboard navigation.
npx shadcn@latest add https://sdk-components.thesqd.com/r/file-tree.jsonimport { FileTree } from "@/components/ui/file-tree";import { FileTree } from "@/components/ui/file-tree";
// A round of work as it lands in Dropbox — brand kit, layered artwork,
// motion project files, and the flattened deliverables.
const items = [
{
name: "Brand",
children: [
{ name: "logo-primary.svg" },
{ name: "logo-stacked.ai" },
{ name: "brand-guidelines.pdf" },
{
name: "Fonts",
children: [
{ name: "Albra-Regular.otf" },
{ name: "Inter-Variable.ttf" },
],
},
],
},
{
name: "Design",
children: [
{ name: "easter-key-art.psd" },
{ name: "title-slide.png" },
{ name: "series-mockup.jpg" },
],
},
{
name: "Motion",
children: [{ name: "bumper.aep" }, { name: "bumper-final.mov" }],
},
{ name: "walk-in-loop.wav" },
{ name: "easter-2026-delivery.zip" },
];
export function Example() {
return (
<FileTree
items={items}
defaultExpandedIds={["Design"]}
onSelect={(node, path) => openAsset(path.join("/"))}
/>
);
}FileTree (single component — pass everything as props)
├── items={FileTreeNode[]} (nested; `children` marks a folder)
│ ├── name ("key-art.psd" — drives the auto icon)
│ ├── id? (defaults to the slash path, "Design/key-art.psd")
│ ├── children? (presence ⇒ folder · type:"folder" for empties)
│ ├── icon? / iconClassName? (override the extension icon)
│ ├── badge? (trailing slot — round count, size, status)
│ ├── hasChildren? (an S3 prefix — opens, children fetched then)
│ ├── data? (your own payload, handed back on onSelect)
│ └── disabled?
├── value / defaultValue (selected node id — the gliding highlight)
├── expandedIds / defaultExpandedIds / defaultExpandAll
├── contextMenu={[…] | (node, path) => […]} (right-click menu)
├── loadChildren={(node, path) => Promise<FileTreeNode[]>} (lazy folders)
├── filter="query" (hides non-matches, forces hit branches open)
├── size="sm | default" · indent={16} · showLines · colorIcons
└── maxHeight={240} (turns the tree into its own scroll-fade scroller)This field is required
Selected: Design/easter-key-art.psd
Right-click any row.
Built from 9 flat keys. Each node’s id is its full object key.
One request per prefix. Open restricted/ to see a failed listing.
sm
no guides
monochrome
The Adobe marks are inlined from the public-domain SVGs on Wikimedia Commons (PD-textlogo) so the CLI install stays a single file. They remain Adobe trademarks and identify the file format only — pass colorIcons={false} to render neutral glyphs instead.
Adobe formats carry the application's own mark:
psd · psb Photoshop
ai · eps Illustrator
indd · idml InDesign
aep · mogrt After Effects
prproj Premiere Pro
pdf Acrobat
Everything else is a tinted Phosphor glyph:
afphoto sky (layered artwork)
afdesign orange (vector)
fig · sketch · xd · ase fuchsia (design tools, palettes)
drp · blend · c4d violet (other motion projects)
mp4 · mov · m4v · webm rose (rendered video)
mp3 · wav · aif cyan (audio)
png · jpg · tif · dng · … teal (raster + raw photography)
svg amber (vector export)
ppt · pptx · key orange (decks)
doc · docx blue
xls · xlsx · csv emerald
otf · ttf · woff neutral (type)
zip · tar · gz · rar neutral (archives)
md · txt neutral
folders neutral (so file types carry the color)↑ / ↓ move to the previous / next visible row
→ open a closed folder, then step into it
← close an open folder, else jump to its parent
Home / End first / last visible row
Enter, Space select the row (and toggle it when it's a folder)| Prop | Type | Default | Description |
|---|---|---|---|
items | FileTreeNode[] | — | The tree. A node is a folder when it has `children` (or `type: "folder"`). |
value | string | null | — | Controlled selection (node id). Leave undefined to let the tree own it. |
defaultValue | string | null | null | Uncontrolled initial selection. |
onValueChange | (id: string) => void | — | Fires with the newly selected node id. |
onSelect | (node: FileTreeNode, path: string[]) => void | — | Fires with the node itself plus its ancestor names, root first — `["Design", "easter-key-art.psd"]`. |
expandedIds | string[] | — | Controlled expansion. Pair with `onExpandedChange`. |
defaultExpandedIds | string[] | [] | Folders open on first render. |
onExpandedChange | (ids: string[]) => void | — | Fires with the full expanded set after every toggle. |
onExpandChange | (id: string, expanded: boolean) => void | — | Fires with just the folder that toggled. |
defaultExpandAll | boolean | false | Start with every folder open. Ignored when `defaultExpandedIds` is set. |
contextMenu | FileTreeMenuItem[] | ((node, path) => FileTreeMenuItem[]) | — | Right-click menu. An array gives every row the same menu; a function varies it per node — return `[]` to suppress it. Right-clicking moves the keyboard cursor without firing `onSelect`. |
onContextMenu | (node: FileTreeNode, path: string[]) => void | — | Fires when a row is right-clicked, before the menu opens. |
onMove | (node: FileTreeNode, target: FileTreeNode | null, path: string[]) => void | — | Drag a row onto another row to move it inside that one, or onto the tree's empty space to move it to the top level (`target` null). Native drag and drop; omit it and nothing drags. The tree only reports the move — rewrite `items` yourself. |
canDrop | (node: FileTreeNode, target: FileTreeNode | null) => boolean | — | Whether `node` may land on `target` (null = the top level). Dropping onto itself, into its own subtree, or a top-level row onto the top level is already refused. |
rowClassName | string | — | Class added to every row. Each row also carries its position in the visible order as `--stagger-index`, which is what lets a caller reveal a listing one row at a time without knowing how long it is. |
rowIndexCap | number | 12 | Rows past this keep the last one's delay, so a long listing doesn't leave its tail waiting seconds for a turn. |
columns | FileTreeColumn[] | — | Columns to the right of every row, under a header naming them — the tree as a table. `{ id, header, width?, align?, cell(node, path) }`; the cell is a function of the node, so nothing has to be copied onto a row to appear in a column. Header type and size match data grid v3's. |
nameHeader | ReactNode | "Name" | Header above the name column. |
showHeader | boolean | — | Draw the header row. Defaults to on whenever there are columns. |
onNodeHover | (node: FileTreeNode, path: string[]) => void | — | Fires when the pointer enters a row or the keyboard cursor lands on it — for warming what a click will need (a preview image, a metadata read) before the click happens. |
loadChildren | (node, path) => Promise<FileTreeNode[]> | — | Fetches a folder's children the first time it opens — one `ListObjectsV2` per prefix instead of walking a whole bucket. Called for folders with no `children` array and for any node marked `hasChildren`. A spinner row shows while in flight, a rejection renders inline, and results are cached for the life of the tree (remount with `key` to drop it). |
loadingMessage | ReactNode | "Loading…" | Row text while a lazy folder is loading. |
filter | string | "" | Substring match over node names. Non-matching files drop out, folders survive when a descendant matches, and matching branches force open. |
colorIcons | boolean | true | Color icons by type. Adobe formats (psd, ai, indd, aep, mogrt, prproj, pdf) render the application's own mark; everything else is a tinted Phosphor glyph — video rose, audio cyan, raster teal, decks orange. `false` renders every icon as a muted Phosphor glyph. |
showLines | boolean | true | Curved branch connectors down each open folder, plus the rail and its selection marker down the left edge. `false` leaves a plain indent. |
indent | number | 16 | Indent per level, in px. |
size | "sm" | "default" | "default" | Row height and type scale (h-7/text-xs vs h-8/text-sm). |
maxHeight | number | string | — | Turns the tree into its own scroller and applies the `scroll-fade` mask. |
emptyMessage | ReactNode | "No files" | Shown when `items` is empty or the filter matches nothing. |
ariaLabel | string | "Files" | Accessible name on the `role="tree"` container. |
className | string | — | Extends the outer wrapper. |
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Displayed label. Its extension picks the Phosphor duotone icon — psd, ai, indd, fig, aep, mogrt, mov, wav, otf, dng and the usual document types are all mapped. |
id | string | slash path | Stable id used for selection and expansion — defaults to `"Design/easter-key-art.psd"`. |
children | FileTreeNode[] | — | Presence marks the node as a folder. |
type | "file" | "folder" | — | Force the kind — use `"folder"` for an empty directory. |
icon | ComponentType<{ className?: string }> | — | Override the extension-derived icon (and its tint). Bind Phosphor `weight` in a wrapper. |
iconClassName | string | — | Tint or resize just this node's icon. |
badge | ReactNode | — | Trailing slot — a round count, an approval Badge, a status dot. |
hasChildren | boolean | — | Marks a folder whose children aren't loaded yet — it opens like any other folder and calls `loadChildren` on first expand. `fileTreeFromListing` sets it on every S3 common prefix. |
data | unknown | — | Your own payload — an S3 object's size, ETag, storage class, version id. FileTree never reads it; `onSelect` hands it back. |
disabled | boolean | — | Dims the row and blocks selection. |
| Prop | Type | Default | Description |
|---|---|---|---|
"separator" | string | — | A divider — pass the bare string in the array. |
heading | ReactNode | — | A group label row: `{ heading: "Danger zone" }`. |
label | ReactNode | — | The item's text. |
icon | ReactNode | — | Leading icon — typically a Phosphor icon element. |
shortcut | ReactNode | — | Right-aligned hint, e.g. "⌘C". Display only — bind the key yourself. |
variant | "default" | "destructive" | "default" | `destructive` tints the item red. |
disabled | boolean | — | Dims and blocks the item. |
onSelect | (node: FileTreeNode, path: string[]) => void | — | Fires with the right-clicked node and its ancestor names, root first. |
| Prop | Type | Default | Description |
|---|---|---|---|
fileTreeFromKeys(objects, options?) | FileTreeNode[] | — | Flat object keys → nested tree. `options`: `root` (prefix to strip), `delimiter` (default `"/"`), `formatMeta` (renders into each file's `badge`), `sort` (folders first, then A–Z — default `true`). Node ids are full keys; a key ending in the delimiter is treated as a folder marker. |
fileTreeFromListing(listing, options?) | FileTreeNode[] | — | One delimited `ListObjectsV2` response → one level. `CommonPrefixes` become folders marked `hasChildren`; `Contents` become files. Pair with `loadChildren`. |
fileTreeFormatSize(bytes) | string | — | `2411724` → `"2.3 MB"`. Handy inside `formatMeta`. |
fileTreeIconFor(name) | ComponentType<{ className?: string }> | — | The Phosphor glyph FileTree would pick for a filename. |
fileTreeBrandIconFor(name) | ComponentType | undefined | — | The full-color Adobe mark for a filename, when one exists. |
fileTreeColorFor(name) | string | — | The tint FileTree would pick, as Tailwind classes. |