Getting Started
Installation
How to install Shadcn Treeview
Shadcn CLI (Recommended)
Use the shadcn CLI to add the tree view component directly to your project:
npx shadcn@latest add https://shadcn-treeview.achromatic.dev/registry/tree-view.jsonThis will:
- Install
react-accessible-treeviewas a dependency - Add
tree-view.tsxto yourcomponents/uidirectory
Package Manager
Alternatively, install as an npm package:
npm install shadcn-treeviewPeer Dependencies
Shadcn Treeview requires React 18 or later:
{
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
}Dependencies
The package includes react-accessible-treeview as a dependency, which provides the accessible tree view primitives. You don't need to install it separately.
Manual Installation
If you prefer to copy the component code directly into your project:
1. Install the dependency
npm install react-accessible-treeview2. Copy the component
Create components/ui/tree-view.tsx and paste the following code:
"use client";
import * as React from "react";
import * as TreeViewPrimitive from "react-accessible-treeview";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: (string | undefined | false)[]) {
return twMerge(clsx(inputs));
}
export type TreeViewProps = React.ComponentPropsWithoutRef<
typeof TreeViewPrimitive.default
>;
const treeViewStyles = `
[role="tree"],
[role="tree"] ul,
[role="group"] {
list-style: none;
margin: 0;
padding: 0;
}
[role="treeitem"] {
list-style: none;
}
`;
const TreeView = React.forwardRef<HTMLUListElement, TreeViewProps>(
(props, ref) => (
<>
<style>{treeViewStyles}</style>
<TreeViewPrimitive.default ref={ref} {...props} />
</>
),
);
TreeView.displayName = "TreeView";
export type TreeViewItemProps = React.ComponentPropsWithoutRef<"div"> & {
level: number;
isExpanded?: boolean;
isBranch?: boolean;
isSelected?: boolean;
indentation?: number;
levelIndentation?: number;
name: string;
icon?: React.ReactNode;
isEditing?: boolean;
onEditSubmit?: (value: string) => void;
isLoading?: boolean;
expandIcon?: React.ReactNode;
loadingIcon?: React.ReactNode;
};
const TreeViewItem = React.forwardRef<HTMLDivElement, TreeViewItemProps>(
(
{
level = 1,
isExpanded = false,
isBranch = false,
isSelected = false,
isLoading = false,
indentation = 16,
levelIndentation = 48,
name = "",
icon,
isEditing = false,
onEditSubmit,
expandIcon,
loadingIcon,
className,
style,
...props
},
ref,
) => {
const [localValueState, setLocalValueState] = React.useState(name);
const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (!inputRef.current?.contains(event.target as Node)) {
onEditSubmit?.(localValueState);
}
};
if (isEditing) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
if (isEditing) {
document.removeEventListener("mousedown", handleClickOutside);
}
};
}, [isEditing, localValueState, onEditSubmit]);
React.useEffect(() => {
if (isEditing) {
inputRef.current?.focus();
}
}, [isEditing]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
onEditSubmit?.(localValueState);
};
const paddingLeft =
level === 1 && !isBranch
? indentation
: levelIndentation * (level - 1) + indentation;
const defaultExpandIcon = (
<svg
aria-hidden="true"
className="text-muted-foreground transition-transform duration-200 group-aria-expanded:rotate-90"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m9 18 6-6-6-6" />
</svg>
);
const defaultLoadingIcon = (
<svg
aria-hidden="true"
className="animate-spin text-muted-foreground"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
return (
<div
ref={ref}
aria-expanded={!isEditing && isExpanded}
className={cn(
"group relative flex h-8 cursor-pointer select-none items-center gap-3 text-sm text-muted-foreground transition-colors hover:bg-muted",
isSelected && "!bg-muted text-foreground",
className,
)}
style={{
paddingLeft,
...style,
}}
data-treeview-is-branch={isBranch}
data-treeview-level={level}
{...props}
>
{level > 1 && (
<div
style={{
left: (levelIndentation / 2 + 4) * (level - 1) + indentation,
}}
className="absolute h-full w-px group-data-[treeview-is-branch=false]:border"
/>
)}
{isSelected && (
<div className="absolute left-0 h-full w-0.5 bg-foreground" />
)}
{isBranch &&
(isLoading
? (loadingIcon ?? defaultLoadingIcon)
: (expandIcon ?? defaultExpandIcon))}
{icon}
<span
className={cn("truncate text-sm", isEditing && "hidden")}
title={name}
>
{name}
</span>
{isEditing && (
<form onSubmit={handleSubmit}>
<input
ref={inputRef}
onChange={(e) => {
setLocalValueState(e.target.value);
}}
onKeyDownCapture={(e) => {
if (e.key === "Enter") {
onEditSubmit?.(localValueState);
} else if (e.key === "Escape") {
setLocalValueState(name);
onEditSubmit?.(name);
} else {
e.stopPropagation();
}
}}
className="block h-7 w-full rounded border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
value={localValueState}
/>
</form>
)}
</div>
);
},
);
TreeViewItem.displayName = "TreeViewItem";
export { TreeView, TreeViewItem };
export { flattenTree } from "react-accessible-treeview";
export type { INode, INodeRendererProps } from "react-accessible-treeview";3. Install utility dependencies (if not already installed)
npm install clsx tailwind-mergeStyling
Shadcn Treeview uses Tailwind CSS classes for styling. Make sure you have Tailwind CSS configured in your project.
The component uses these CSS variables for theming (compatible with Shadcn UI):
--muted/--muted-foreground- For item backgrounds and text--foreground- For selected item indicator--background- For input backgrounds--ring- For focus rings
If you're using Shadcn UI, these variables are already configured.