Collaboration
Project Management
Finance
Tree
A composable tree component with animated expand/collapse and customizable nodes for displaying hierarchical data structures.
Installation
npx love-ui@latest add treeUsage
import { TreeProvider, TreeView, TreeNode, TreeNodeTrigger, TreeExpander, TreeIcon, TreeLabel, TreeNodeContent, TreeLines } from "@/components/tree"<TreeProvider>
<TreeView>
<TreeLines />
{items.map((item) => (
<TreeNode key={item.id} id={item.id}>
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>{item.name}</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent>
{/* Nested items */}
</TreeNodeContent>
</TreeNode>
))}
</TreeView>
</TreeProvider>Features
- Animated expand/collapse with configurable animations
- Customizable node icons and labels
- Single and multi-selection support
- Optional tree lines for visual hierarchy
- Keyboard navigation support (Ctrl/Cmd for multi-select)
- Fully composable API for custom tree nodes
- Built-in folder/file icons with open/closed states
Examples
Simple tree
Documents
Downloads
"use client";
import {
TreeExpander,
TreeIcon,
TreeLabel,
TreeNode,
TreeNodeContent,
TreeNodeTrigger,
TreeProvider,
TreeView,
} from "../../../../../packages/tree";
export default function TreeSimpleExample() {
return (
<TreeProvider>
<TreeView>
<TreeNode nodeId="documents">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Documents</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={1} nodeId="work">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Work</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="project-a">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Project A.pdf</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="project-b">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Project B.pdf</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode isLast level={1} nodeId="personal">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Personal</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="resume">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Resume.docx</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="cover-letter">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Cover Letter.docx</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode isLast nodeId="downloads">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Downloads</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={1} nodeId="installer">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>installer.exe</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={1} nodeId="update">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>update.zip</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeView>
</TreeProvider>
);
}
Custom icons
Database
Users
id
email
password
Roles
Admin
User
API
Authentication
Users Management
"use client";
import {
TreeExpander,
TreeIcon,
TreeLabel,
TreeNode,
TreeNodeContent,
TreeNodeTrigger,
TreeProvider,
TreeView,
} from "../../../../../packages/tree";
import {
Database,
Globe,
Key,
Lock,
Server,
Shield,
Table,
User,
Users,
} from "lucide-react";
export default function TreeCustomIconsExample() {
return (
<TreeProvider
defaultExpandedIds={["database", "users-table", "roles-table", "api"]}
>
<TreeView>
<TreeNode nodeId="database">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon
hasChildren
icon={<Database className="h-4 w-4 text-blue-500" />}
/>
<TreeLabel>Database</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={1} nodeId="users-table">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon
hasChildren
icon={<Table className="h-4 w-4 text-green-500" />}
/>
<TreeLabel>Users</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="id-field">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon
icon={<Key className="h-4 w-4 text-yellow-500" />}
/>
<TreeLabel>id</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode level={2} nodeId="email-field">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon
icon={<Globe className="h-4 w-4 text-purple-500" />}
/>
<TreeLabel>email</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="password-field">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon
icon={<Lock className="h-4 w-4 text-red-500" />}
/>
<TreeLabel>password</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode isLast level={1} nodeId="roles-table">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon
hasChildren
icon={<Table className="h-4 w-4 text-green-500" />}
/>
<TreeLabel>Roles</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="admin-role">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon
icon={<Shield className="h-4 w-4 text-orange-500" />}
/>
<TreeLabel>Admin</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="user-role">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon
icon={<User className="h-4 w-4 text-blue-400" />}
/>
<TreeLabel>User</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode isLast nodeId="api">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon
hasChildren
icon={<Server className="h-4 w-4 text-indigo-500" />}
/>
<TreeLabel>API</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={1} nodeId="auth-endpoint">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon icon={<Lock className="h-4 w-4 text-red-500" />} />
<TreeLabel>Authentication</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={1} nodeId="users-endpoint">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon icon={<Users className="h-4 w-4 text-blue-500" />} />
<TreeLabel>Users Management</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeView>
</TreeProvider>
);
}
Without lines
Projects
Website Redesign
Homepage
About Page
Contact Form
Mobile App
Resources
"use client";
import {
TreeExpander,
TreeIcon,
TreeLabel,
TreeNode,
TreeNodeContent,
TreeNodeTrigger,
TreeProvider,
TreeView,
} from "../../../../../packages/tree";
export default function TreeNoLinesExample() {
return (
<TreeProvider
defaultExpandedIds={["projects", "website-redesign"]}
showLines={false}
>
<TreeView>
<TreeNode nodeId="projects">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Projects</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={1} nodeId="website-redesign">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Website Redesign</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="homepage">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Homepage</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode level={2} nodeId="about-page">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>About Page</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="contact-form">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Contact Form</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode isLast level={1} nodeId="mobile-app">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Mobile App</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="ios-version">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>iOS Version</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="android-version">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Android Version</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode isLast nodeId="resources">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Resources</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={1} nodeId="documentation">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Documentation</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode level={1} nodeId="api-reference">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>API Reference</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={1} nodeId="examples">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Examples</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeView>
</TreeProvider>
);
}
Controlled selection
Team
Engineering
Connor Love
Ian Schroeder
Design
Owen Caudy
Tyler PennyPacker
Product
Tyler Love
Ben Dyer
"use client";
import {
TreeExpander,
TreeIcon,
TreeLabel,
TreeNode,
TreeNodeContent,
TreeNodeTrigger,
TreeProvider,
TreeView,
} from "../../../../../packages/tree";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export default function TreeControlledExample() {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [expandedIds] = useState<string[]>([
"team",
"engineering",
"design",
"product",
]);
const handleClearSelection = () => {
setSelectedIds([]);
};
const handleSelectAll = () => {
const allIds = ["alice", "bob", "carol", "david", "eve", "frank"];
setSelectedIds(allIds);
};
return (
<div className="space-y-4">
<div className="flex gap-2">
<Button onClick={handleSelectAll} size="sm" variant="outline">
Select All Team Members
</Button>
<Button onClick={handleClearSelection} size="sm" variant="outline">
Clear Selection
</Button>
</div>
<TreeProvider
defaultExpandedIds={expandedIds}
multiSelect
onSelectionChange={setSelectedIds}
selectedIds={selectedIds}
>
<TreeView>
<TreeNode isLast nodeId="team">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Team</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={1} nodeId="engineering">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Engineering</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="alice">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Connor Love</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="bob">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Ian Schroeder</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode level={1} nodeId="design">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Design</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="carol">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Owen Caudy</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="david">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Tyler PennyPacker</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
<TreeNode isLast level={1} nodeId="product">
<TreeNodeTrigger>
<TreeExpander hasChildren />
<TreeIcon hasChildren />
<TreeLabel>Product</TreeLabel>
</TreeNodeTrigger>
<TreeNodeContent hasChildren>
<TreeNode level={2} nodeId="eve">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Tyler Love</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
<TreeNode isLast level={2} nodeId="frank">
<TreeNodeTrigger>
<TreeExpander />
<TreeIcon />
<TreeLabel>Ben Dyer</TreeLabel>
</TreeNodeTrigger>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeNodeContent>
</TreeNode>
</TreeView>
</TreeProvider>
{selectedIds.length > 0 && (
<div className="text-muted-foreground text-sm">
Selected: {selectedIds.join(", ")}
</div>
)}
</div>
);
}