Tree View React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React tree view for folders, categories or permissions: animated expand, selection, tri-state checkboxes, filter and drag to move.
Click a row to select it; clicking a folder also opens or closes it while its children slide in and the chevron turns on a quick spring, and the chevron alone toggles too. Double click a file (or tap it on a phone) to open it. In multiple mode clicks toggle rows and Shift + click selects a range. With checkboxes, clicking a box checks it and, with cascade, everything under it; parents turn to a dash when only some children are checked. With selectionMode none a click on a file row checks it too, while a click on a folder row only opens it. Keyboard (one Tab stop): ArrowDown and ArrowUp move between visible rows, ArrowRight opens a closed folder or steps into an open one, ArrowLeft closes an open folder or jumps to the parent, Home and End go to the first and last row, PageUp and PageDown jump ten rows, Enter opens a file or toggles a folder, Space selects (or checks with checkboxes), Shift + arrows extend a multiple selection, Ctrl+A (Cmd+A on a Mac) selects every visible row, * opens every folder beside the focused one, typing letters jumps to the next row that starts with them (diacritics ignored), Escape clears a multiple selection, and Shift+F10 or the ContextMenu key opens the row's ⋯ menu. With the filter field, typing filters as you go: matches keep their parents, the path to them opens and matched letters turn bold; ArrowDown moves into the tree and Escape clears the field. Folders with lazy children show a spinner row while they load and a Retry row when loading fails. With draggable on, drag a row with the mouse (or long press it on touch) and a line shows where it lands: before or after a row, or a ring around a folder it goes into; hovering a closed folder opens it, and dropping a folder into itself is refused. Alt + ArrowUp and Alt + ArrowDown move the focused row among its siblings, Alt + ArrowRight moves it into the folder above and Alt + ArrowLeft moves it out one level; every move is announced. On a phone rows grow to 44px, the ⋯ button is always visible and opens a bottom sheet, the whole row is the tap target and the filter field uses 16px text. Huge trees only render the rows in view.
Tree View is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add tree-view, together with Button, Checkbox, Dropdown Menu, Empty State, Input and 8 more and the npm packages @dnd-kit/core, @tanstack/react-virtual, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add tree-viewTree View is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import TreeView from @/components/ui/tree-view.
import { useState } from "react"
import { TreeView, moveTreeNode, type TreeNode } from "@/components/ui/tree-view"
const documents: TreeNode[] = [
{
id: "clients",
label: "Clients",
children: [
{
id: "tech",
label: "Tech Solutions Inc.",
children: [
{ id: "contract", label: "Contract 2026.pdf" },
{ id: "invoice", label: "Invoice 2026-0412.pdf" },
],
},
{ id: "logistic", label: "Summit Logistics", hasChildren: true },
],
},
{ id: "reports", label: "Reports", children: [{ id: "q3", label: "Q3 2026.xlsx" }] },
]
export function Documents({ onOpen }: { onOpen: (id: string) => void }) {
const [items, setItems] = useState(documents)
return (
<TreeView
items={items}
folderIcons
searchable
draggable
defaultExpandedKeys={["clients", "tech"]}
loadChildren={async () => [{ id: "framework", label: "Framework contract 2025.pdf" }]}
onAction={(node) => onOpen(node.id)}
onMove={(event) => setItems((prev) => moveTreeNode(prev, event))}
labels={{ tree: "Documents", search: "Search documents" }}
/>
)
}Props
items*TreeNode[]The tree: { id, label, textValue?, icon?, expandedIcon?, children?, hasChildren?, disabled?, badge?, description?, href?, actions?: MenuEntry[], draggable?, droppable? }. Ids must be unique across the whole tree (a dev warning names duplicates). A non-string label needs textValue for filtering and type-ahead.
selectionModedefault"single""none" | "single" | "multiple"none: rows only open and act. single: one selected row. multiple: clicks toggle rows, Shift extends a range, Ctrl/Cmd+A selects every visible row.
selectedKeysstring[]Selected ids (controlled). Pair with onSelectedKeysChange.
defaultSelectedKeysstring[]Selected ids while uncontrolled. Default [].
onSelectedKeysChange(keys: string[]) => voidFires with the new selection, in display order.
checkboxesdefaultfalsebooleanTri-state checkboxes before the icons, independent of selection: the box checks, the row selects.
checkedKeysstring[]Checked ids (controlled). With cascade, fully checked parents are part of the keys too.
defaultCheckedKeysstring[]Checked ids while uncontrolled. Default [].
onCheckedKeysChange(keys: string[]) => voidFires with the new checked ids.
cascadedefaulttruebooleanChecking a parent checks its (loaded, enabled) descendants; a parent is checked when all of them are and shows a dash when some are. Disabled rows are skipped. Needs checkboxes.
expandedKeysstring[]Open folder ids (controlled). Ids of nodes that do not exist are ignored.
defaultExpandedKeysstring[]Open folder ids while uncontrolled. Default [].
onExpandedKeysChange(keys: string[]) => voidFires when a folder opens or closes, including the folders a filter opens.
defaultExpandAlldefaultfalsebooleanStarts with every folder that has children open (lazy folders stay closed).
expandOnClickdefaulttruebooleanClicking a folder row also opens or closes it, like a file explorer. Off: only the chevron and the keyboard do.
loadChildren(node: TreeNode) => Promise<TreeNode[]>Lazy children: a node with hasChildren and no children loads on first expand, with a spinner row; a rejected promise shows a Retry row. Results are cached inside the tree. Filtering only sees loaded nodes.
onAction(node: TreeNode) => voidEnter, double click or a tap on a leaf: open the file. Without it a node with href follows its link.
searchabledefaultfalsebooleanA filter field above the tree (16px text on phones, Escape clears it, ArrowDown enters the tree).
filterdefault""stringThe filter text (controlled, pair with onFilterChange); works without the field too. Case and diacritics never matter, matches keep their ancestors and open the path to them, debounced 150ms.
onFilterChange(filter: string) => voidFires on every keystroke in the filter field.
sizedefault"md""sm" | "md" | "lg"Rows 28 / 32 / 36px with 13 / 14 / 15px text; 44px on touch screens.
indentdefault16numberPixels per level.
guidesdefaulttruebooleanVertical indent guides; the guide of the focused row's branch is darker.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The selected row (neutral is a gray tint, other tones their soft fill and text) and the checkboxes.
colordefault""stringAny CSS color; replaces the tone for the selected row and the checkboxes.
folderIconsdefaultfalsebooleanFolder / open folder icons for parents and a file icon for leaves, for nodes without their own icon.
draggabledefaultfalsebooleanDrag rows (long press on touch) to reorder them or drop them into folders, and Alt + arrows to move with the keyboard. Needs onMove.
onMove(event: TreeMoveEvent) => void{ id, targetId, position: "before" | "after" | "inside", fromParentId, toParentId, index }. index is the position among the new siblings; moveTreeNode(items, event) applies it to your data.
virtualizedefault"auto"boolean | "auto" | "always" | "never"auto renders only the rows in view above 300 visible rows; true / always and false / never force it. Virtualized trees are flat (aria-level, aria-setsize, aria-posinset), skip the expand animation and default to a 420px scroll box.
maxHeightdefault0number | stringMakes the tree its own scroll box (px or any CSS length). 0 grows with the rows.
emptyTextdefault"No items"stringTitle of the empty state shown in place of the tree when items is empty. A filter without matches shows labels.noResults with a Clear filter button instead.
emptyStateReactNodeReplaces the built-in empty state (an EmptyState with emptyText as its title) when items is empty.
labelsPartial<TreeViewLabels>Strings: tree (accessible name), search, noResults, clearFilter, loading, loadError, retry, actions, cancel (the phone sheet), and the move announcements pickedUp, dropBefore, dropAfter, dropInside, notAllowed, dropped, cancelled, moved ({item}, {target}, {position}, {total}). English defaults in DEFAULT_TREE_VIEW_LABELS.
classNamesPartial<Record<"search" | "viewport" | "root" | "item" | "row" | "guides" | "chevron" | "checkbox" | "icon" | "label" | "description" | "badge" | "actions" | "group" | "loading" | "error" | "empty" | "overlay", string>>Extra classes for the inner parts.
classNamestringExtra classes for the wrapper, merged last.
Parts
flattenTree
functionDepth-first rows with level, parentId, ancestors, index and setSize; with expandedKeys only the visible ones. Signature: (items: TreeNode[], expandedKeys?: Iterable<string>) => FlatTreeNode[].
findNode
functionFinds a node anywhere in the tree. Signature: (items: TreeNode[], id: string) => TreeNode | undefined.
getAncestorIds
functionThe parent chain of a node, top level first. Handy for expandedKeys that reveal a node. Signature: (items: TreeNode[], id: string) => string[].
getCheckedState
functionThe cascade state of a node, the same rule the checkboxes use. Signature: (node: TreeNode, checkedKeys: Iterable<string>) => boolean | "indeterminate".
moveTreeNode
functionApplies an onMove event to your data and returns new items; untouched branches keep their identity. Signature: (items: TreeNode[], event: TreeMoveEvent) => TreeNode[].
DEFAULT_TREE_VIEW_LABELS
constantThe English strings for the filter, no matches, loading and retry, the row actions menu and the drag and drop announcements. Override keys through labels.
TreeNode
typeOne node of items: an id, a label and optional children, plus the per-node extras described under items.
TreeMoveEvent
typeWhat onMove receives after a drop: { id, targetId, position ("before", "after" or "inside"), fromParentId, toParentId, index }.
TreeViewProps
typeThe props of TreeView, for wrappers that pass them through.
Examples
Permissions with cascade
Checking a group checks everything under it; partly checked groups show a dash. Disabled rows are skipped.
Searchable categories
Matches keep their parents, open the path to them and bold the matched letters.
Drag to move files
Drop before, after or into a folder; onMove plus moveTreeNode updates your data. A folder opens if you hover it.
5,000 rows, virtualized
Above 300 visible rows only what is in view renders; maxHeight makes the tree its own scroll box.
Compact sidebar
size="sm", no guides, a brand tone for the selected row and icons from the data.
Lazy children with retry
Children load on first expand. The FTP folder fails once, then Retry loads it.
Dependencies
Files
- components/ui/tree-view.tsx
FAQ
Is the React Tree View component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Tree View?
Run npx wingo-ui@latest add tree-view. It also adds the Wingo UI items it builds on (Button, Checkbox, Dropdown Menu, Empty State, Input and 8 more) and the npm packages @dnd-kit/core, @tanstack/react-virtual, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/tree-view.
How does Tree View behave on phones?
Double click a file (or tap it on a phone) to open it. On a phone rows grow to 44px, the ⋯ button is always visible and opens a bottom sheet, the whole row is the tap target and the filter field uses 16px text.
See also
Used in
More React data display components
Articles using this component
- React File Manager: Folders, Uploads and the Backend API
Component guides10 min read
- React Tree View: Checkboxes, Lazy Loading, Drag to Move
Component guides10 min read