Tree
A flexible tree component for hierarchical data display with expand/collapse functionality and selection states.
Demo
"use client";
import { File } from "lucide-react";
import { useState } from "react";
import {
TreeProvider,
TreeView,
TreeNode,
TreeNodeTrigger,
TreeNodeContent,
TreeExpander,
TreeIcon,
TreeLabel,
} from "@/components/domain-ui/tree";
interface TreeNodeData {
name: string;
children: TreeNodeData[];
metadata: { path: string };
}
// Sample file structure
const sampleFiles = [
{
name: "src",
path: "/src",
type: "directory" as const,
children: [
{
name: "components",
path: "/src/components",
type: "directory" as const,
children: [
{
name: "button.tsx",
path: "/src/components/button.tsx",
type: "file" as const,
},
{
name: "card.tsx",
path: "/src/components/card.tsx",
type: "file" as const,
},
{
name: "dialog.tsx",
path: "/src/components/dialog.tsx",
type: "file" as const,
},
],
},
{
name: "lib",
path: "/src/lib",
type: "directory" as const,
children: [
{
name: "utils.ts",
path: "/src/lib/utils.ts",
type: "file" as const,
},
{
name: "cn.ts",
path: "/src/lib/cn.ts",
type: "file" as const,
},
],
},
{
name: "app.tsx",
path: "/src/app.tsx",
type: "file" as const,
},
],
},
{
name: "public",
path: "/public",
type: "directory" as const,
children: [
{
name: "favicon.ico",
path: "/public/favicon.ico",
type: "file" as const,
},
{
name: "robots.txt",
path: "/public/robots.txt",
type: "file" as const,
},
],
},
{
name: "package.json",
path: "/package.json",
type: "file" as const,
},
{
name: "README.md",
path: "/README.md",
type: "file" as const,
},
];
const findFirstFile = (
nodes: typeof sampleFiles
): (typeof sampleFiles)[0] | null => {
for (const node of nodes) {
if (node.type === "file") {
return node;
}
if (node.children) {
const foundFile = findFirstFile(node.children);
if (foundFile) {
return foundFile;
}
}
}
return null;
};
export default function TreeDemo() {
const [selectedFile, setSelectedFile] = useState<
(typeof sampleFiles)[0] | null
>(findFirstFile(sampleFiles));
// Initialize with the first file's name
const firstFile = findFirstFile(sampleFiles);
const [selectedIds, setSelectedIds] = useState<string[]>(
firstFile ? [firstFile.name] : []
);
// Convert sampleFiles to tree format
const convertToTreeFormat = (nodes: typeof sampleFiles): any => {
return {
name: "",
children: nodes.map((node) => ({
name: node.name,
children: node.children
? convertToTreeFormat(node.children).children
: [],
})),
};
};
const treeData = convertToTreeFormat(sampleFiles);
// Handle selection change for tree
const handleSelectionChange = (selectedIds: string[]) => {
setSelectedIds(selectedIds);
if (selectedIds.length > 0) {
const selectedName = selectedIds[0];
// Find the selected file in sampleFiles
const findFileByName = (
nodes: typeof sampleFiles,
name: string
): (typeof sampleFiles)[0] | null => {
for (const node of nodes) {
if (node.name === name) {
return node;
}
if (node.children) {
const found = findFileByName(node.children, name);
if (found) {
return found;
}
}
}
return null;
};
const foundFile = findFileByName(sampleFiles, selectedName);
if (foundFile && foundFile.type === "file") {
setSelectedFile(foundFile);
}
}
};
const renderTreeNode = (
item: any,
level = 1,
isLast = false
): React.ReactNode => {
const hasChildren = item.children && item.children.length > 0;
const nodeId = item.name || "root";
if (!item.name) {
return item.children?.map((child: any, index: number) =>
renderTreeNode(child, level, index === item.children.length - 1)
);
}
return (
<TreeNode key={nodeId} nodeId={nodeId} level={level} isLast={isLast}>
<TreeNodeTrigger>
<TreeExpander hasChildren={hasChildren} />
<TreeIcon
hasChildren={hasChildren}
icon={
hasChildren ? undefined : (
<File strokeWidth={1.5} size={16} className="shrink-0" />
)
}
/>
<TreeLabel>{item.name}</TreeLabel>
</TreeNodeTrigger>
{hasChildren && (
<TreeNodeContent hasChildren={hasChildren}>
{item.children?.map((child: any, index: number) => {
// All children should be at the next level
return renderTreeNode(
child,
level + 1,
index === item.children.length - 1
);
})}
</TreeNodeContent>
)}
</TreeNode>
);
};
return (
<div className="space-y-4">
<h3 className="text-sm font-medium">File Tree</h3>
<div className="w-64 h-[400px] py-2 border rounded-lg bg-muted/30 overflow-auto">
<TreeProvider
selectedIds={selectedIds}
onSelectionChange={handleSelectionChange}
defaultExpandedIds={["src", "components", "lib", "public"]}
indent={38}
>
<TreeView className="min-w-fit">{renderTreeNode(treeData)}</TreeView>
</TreeProvider>
</div>
<div className="rounded-lg border bg-muted/30 p-3">
<p className="text-xs text-muted-foreground">
Selected: {selectedFile?.name || "None"}
</p>
</div>
</div>
);
}
Installation
CLI
pnpm dlx shadcn@latest add https://domain-ui.dev/r/tree.json
npx shadcn@latest add https://domain-ui.dev/r/tree.json
npx shadcn@latest add https://domain-ui.dev/r/tree.json
bunx --bun shadcn@latest add https://domain-ui.dev/r/tree.json
Manual
Copy and paste the following code into your project:
Loading registry files...
Usage
import {
TreeProvider,
TreeView,
TreeNode,
TreeNodeTrigger,
TreeNodeContent,
TreeExpander,
TreeIcon,
TreeLabel,
} from "@/components/ui/tree";
export default function MyComponent() {
return (
<TreeProvider defaultExpandedIds={["src"]}>
<TreeView>
<TreeNode nodeId="src" level={1}>
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>src</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode nodeId="app.tsx" level={2}>
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>app.tsx</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode nodeId="main.tsx" level={2}>
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>main.tsx</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode nodeId="package.json" level={1}>
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>package.json</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeView>
</TreeProvider>
);
}
API Reference
TreeProvider
The root provider that manages tree state.
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | Tree content |
defaultExpandedIds | string[] | [] | Initially expanded nodes |
showLines | boolean | true | Show connecting lines |
showIcons | boolean | true | Show node icons |
selectable | boolean | true | Enable node selection |
multiSelect | boolean | false | Allow multiple selections |
selectedIds | string[] | - | Controlled selected nodes |
onSelectionChange | (ids: string[]) => void | - | Selection change handler |
indent | number | 19 | Indentation per level |
animateExpand | boolean | true | Animate expand/collapse |
TreeNode
Individual tree node container.
Prop | Type | Default | Description |
---|---|---|---|
nodeId | string | - | Unique node identifier |
level | number | 0 | Node depth level |
isLast | boolean | false | Is last child |
children | ReactNode | - | Node content |
TreeNodeTrigger
Clickable node trigger that handles selection and expansion.
TreeExpander
Chevron icon that indicates and controls node expansion.
Prop | Type | Default | Description |
---|---|---|---|
hasChildren | boolean | false | Whether node has children |
TreeIcon
Node icon that changes based on node type and state.
Prop | Type | Default | Description |
---|---|---|---|
icon | ReactNode | - | Custom icon |
hasChildren | boolean | false | Whether node has children |
TreeLabel
Text label for the node.
TreeNodeContent
Container for child nodes with expand/collapse animation.
Last updated on