A file and folder tree built on Tree, from a list of nodes. Folders show open and closed icons, files get an icon by extension, and each row can carry a size or a status badge.
import { FileTree, type FileTreeNode } from "@rdloom/react";
const items: FileTreeNode[] = [
{
id: "src",
name: "src",
children: [
{
id: "components",
name: "components",
children: [
{ id: "card", name: "card.tsx" },
{ id: "menu", name: "menu.tsx" },
],
},
{ id: "index", name: "index.ts" },
{ id: "logo", name: "logo.png" },
],
},
{ id: "docs", name: "docs", children: [{ id: "readme", name: "README.md" }] },
{ id: "package", name: "package.json" },
{ id: "license", name: "LICENSE" },
];
export default function FileTreeBasicExample() {
return (
<div className="flex w-full justify-center">
<div className="w-72">
<FileTree label="Project files" items={items} defaultExpandedKeys={["src"]} />
</div>
</div>
);
}Installation
npx rdloom add file-treeCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/file-tree.jsonWorks, but without upgrade tracking.
Usage
import { FileTree, type FileTreeNode } from "@rdloom/react";
<div className="flex w-full justify-center">
<div className="w-72">
<FileTree label="Project files" items={items} defaultExpandedKeys={["src"]} />
</div>
</div>With sizes and status
import { FileTree, type FileTreeNode } from "@rdloom/react";
const items: FileTreeNode[] = [
{
id: "app",
name: "app",
children: [
{ id: "page", name: "page.tsx", size: "2.1 KB", status: "modified" },
{ id: "layout", name: "layout.tsx", size: "1.4 KB" },
{ id: "styles", name: "styles.css", size: "6.8 KB", status: "added" },
],
},
{ id: "notes", name: "notes.txt", size: "312 B", status: "deleted" },
{ id: "config", name: "config.json", size: "0.9 KB" },
];
export default function FileTreeWithSizesAndStatusExample() {
return (
<div className="flex w-full justify-center">
<div className="w-80">
<FileTree label="Changed files" items={items} defaultExpandedKeys={["app"]} />
</div>
</div>
);
}Controlled
Selected: main. Last opened: none.
import { useState } from "react";
import { FileTree, type FileTreeNode } from "@rdloom/react";
const items: FileTreeNode[] = [
{ id: "assets", name: "assets", children: [{ id: "hero", name: "hero.webp" }, { id: "icon", name: "icon.svg" }] },
{ id: "main", name: "main.py" },
{ id: "readme", name: "README.md" },
];
export default function FileTreeControlledExample() {
const [selected, setSelected] = useState<Set<string>>(new Set(["main"]));
const [open, setOpen] = useState<Set<string>>(new Set(["assets"]));
const [last, setLast] = useState("");
return (
<div className="flex w-full flex-col items-center gap-3">
<div className="w-72">
<FileTree
label="Controlled files"
items={items}
selectedKeys={selected}
onSelectionChange={(keys) => setSelected(keys === "all" ? new Set() : new Set([...keys].map(String)))}
expandedKeys={open}
onExpandedChange={(keys) => setOpen(new Set([...keys].map(String)))}
onAction={(key) => setLast(String(key))}
/>
</div>
<p className="text-sm text-[var(--rd-color-text-muted)]" aria-live="polite">
Selected: {[...selected].join(", ") || "nothing"}. Last opened: {last || "none"}.
</p>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
labelrequiredAccessible name of the tree, e.g. "Project files". | string | none |
itemsrequiredThe nodes. A node with children is a folder; the id is its key and is unique across the whole tree. | FileTreeNode[] | none |
selectionModeWhether rows can be selected. | "none" | "single" | "multiple" | "single" |
selectedKeysControlled selection, by node id. | "all" | Iterable<Key> | none |
defaultSelectedKeysInitial selection when uncontrolled. | "all" | Iterable<Key> | none |
onSelectionChangeCalled with the selected node ids. | (keys: "all" | Set<Key>) => void | none |
expandedKeysControlled set of open folders, by id. | Iterable<Key> | none |
defaultExpandedKeysFolders open at first. | Iterable<Key> | none |
onExpandedChangeCalled when folders are opened or closed. | (keys: Set<Key>) => void | none |
onActionCalled when a row is activated with Enter or a double-click. | (key: Key) => void | none |
Accessibility
Role tree, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab moves into the tree, on one row, and out again
- Up and Down arrows move between visible rows
- Right arrow opens a closed folder, or moves to its first child; Left arrow closes it, or moves to its parent
- Home and End jump to the first and last visible row
- Enter activates the row (onAction); Space selects it when selection is on
- Typing a letter jumps to the next row starting with it
Screen readers announce
- Entering announces the tree name, the row, its level and position, and whether a folder is open
- A row with a status is read as its name followed by the status
- Opening or closing a folder announces the new state
What your code must do
- Built on Tree: a named tree whose rows expose aria-level, aria-expanded (folders) and aria-selected (when selectable)
- File and folder icons are decoration (aria-hidden); the name is the text of the row
- A status such as modified is part of the row accessible name, not only a coloured badge, and the badge shows a letter as well as a colour
- Sizes are text you supply, already formatted for your locale
- Selection is shown by a filled row and aria-selected, not only colour
Guidelines
Use it when
- Showing a project's files and folders
- Picking a file from a hierarchy, with an optional size or change status
Avoid it when
- Flat file lists: use a ListBox or Table
- Folders with thousands of children loaded at once
Don't
- Reusing the same id for two nodes
- Showing the status only by colour
- Using the file extension as the only way to tell what a file is
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-default--rd-color-surface-subtle--rd-color-surface-selected--rd-color-border-default--rd-color-text-default--rd-color-text-muted--rd-color-focus-ring--rd-radius-control