30 / Documents
File Tree
An expandable tree of folders and files with full keyboard navigation and correct tree semantics.
"use client"
import { FileTree } from "mischief-ui/file-tree"
const nodes = [
{
id: "contracts",
name: "contracts",
kind: "folder" as const,
children: [
{
id: "2026",
name: "2026",
kind: "folder" as const,
children: [
{ id: "acme", name: "acme-msa.pdf", meta: "1.2 MB" },
{ id: "northwind", name: "northwind-sow.pdf", meta: "840 KB" },
],
},
{ id: "archive", name: "archive", kind: "folder" as const, children: [] },
],
},
{
id: "invoices",
name: "invoices",
kind: "folder" as const,
children: [{ id: "jan", name: "january.pdf", meta: "94 KB" }],
},
{ id: "readme", name: "readme.md", meta: "2 KB" },
]
export function FileTreeDemo() {
return (
<FileTree
className="w-full max-w-sm"
nodes={nodes}
defaultExpandedIds={["contracts", "2026"]}
defaultSelectedId="acme"
/>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/file-treeimport { FileTree } from "mischief-ui/file-tree"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { ChevronRight, File, Folder, FolderOpen } from "lucide-react"import { cn } from "@/lib/utils" export type FileTreeNode = { id: string name: string kind?: "file" | "folder" children?: FileTreeNode[] meta?: React.ReactNode icon?: React.ReactNode}Usage
const nodes = [
{
id: "invoices",
name: "invoices",
kind: "folder",
children: [{ id: "jan", name: "january.pdf" }],
},
]
export function Files() {
return <FileTree nodes={nodes} onSelect={openFile} />
}Building the tree
Nodes nest through children. A node is treated as a folder when it has a children array -- including an empty one -- so an empty folder is spelled children: [] rather than left out. Set kind explicitly when you want a folder that has not loaded its contents yet to still look like a folder.
const nodes = [
{
id: "app",
name: "app",
children: [
{ id: "app/page.tsx", name: "page.tsx", meta: "2.4 kB" },
{ id: "app/api", name: "api", children: [] },
],
},
{ id: "README.md", name: "README.md" },
]Ids must be unique across the whole tree, not just among siblings, because expansion and selection are tracked by id. Paths make good ids for that reason.
Loading children on demand
The component renders the nodes it is given and does not fetch anything. To fill a folder when it opens, control expansion and replace that node's children as the answer arrives.
const [expandedIds, setExpandedIds] = useState<string[]>([])
async function onExpandedChange(ids: string[]) {
const opened = ids.find((id) => !expandedIds.includes(id))
setExpandedIds(ids)
if (opened && !loaded.has(opened)) {
setNodes(await withChildren(opened, await listDirectory(opened)))
}
}API
nodesFileTreeNode[]Id, name, kind, optional children, meta, and icon.expandedIds, defaultExpandedIdsstring[]Which folders are open, controlled or uncontrolled.selectedId, defaultSelectedIdstring | nullThe selected node, controlled or uncontrolled.onSelect(node: FileTreeNode) => voidRuns on selection.onExpandedChange(ids: string[]) => voidRuns when a folder opens or closes.FileTreeNode
idstringUnique across the whole tree. Paths work well.namestringThe label.kind"file" | "folder"Overrides the guess made from children.childrenFileTreeNode[]Present, even empty, means a folder.metaReactNodeTrailing detail such as a size or a status.iconReactNodeReplaces the default file or folder mark.Accessibility
The tree uses tree and treeitem roles with aria-level and aria-expanded on every row, so depth and state are announced rather than implied by indentation. There is one tab stop into the tree. Up and Down move between visible rows, Right opens a folder or steps into it, Left closes it or moves to its parent, Home and End jump to the ends, and Enter or Space selects.