Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Component guides
  4. React Tree View: Checkboxes, Lazy Loading, Drag to Move
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Tree View: Checkboxes, Lazy Loading, Drag to Move
Component guides
Component guides

React Tree View: Checkboxes, Lazy Loading, Drag to Move

SR
Serban Rusu · Founder of Wingo UI
Oct 19, 2026 · 10 min read

On this page

0%
  1. What should a React tree view handle?
  2. How do the React tree view libraries compare?
  3. What does the Tree View need to render?
  4. How do tri-state checkboxes work in a React tree view?
  5. How do I load children lazily?
  6. How does the filter work?
  7. How do I add drag and drop to a React tree view?
  8. Which keyboard shortcuts does a tree view need?
  9. How does it behave on a phone and with big trees?
  10. What are the common tree view pitfalls?
  11. When should I use the File Manager instead?
  12. How do I add it to my project?
  13. FAQ
    1. Does shadcn/ui have a tree view component?
    2. How do I add checkboxes to a React tree view?
    3. How do I load tree children lazily in React?
    4. How do I convert a flat list with parentId into a tree?
    5. Is the Wingo UI Tree View free?

TL;DR

A usable React tree view needs the WAI-ARIA tree keyboard model, tri-state checkboxes that cascade, children that load on first expand, a filter that keeps the path to each match, and a drag to move with a keyboard alternative. The Wingo UI Tree View does all of it from one items array of nested nodes; the File Manager builds a full documents screen on top of it.

Published Oct 19, 2026

You need a React tree view for folders, product categories or a permissions screen. A recursive component with a chevron and some indentation takes ten minutes. Then the requirements show up: a checkbox on a folder should check everything under it, a branch with 2,000 children should load only when it opens, a filter should find "invoice" three levels deep, someone wants to drag a file into another folder, and the whole thing has to work with arrow keys and on a phone. This guide covers what a production tree needs, how the common options compare, and the props that matter in the Wingo UI Tree View, with working code for each part.

What should a React tree view handle?

Seven things. A recursive component with a chevron covers only the first:

  1. Expand and collapse with an animation that does not jump the page.
  2. Keyboard navigation following the WAI-ARIA tree view pattern (opens in a new tab): one Tab stop, arrows to move, Right to open, Left to close or go to the parent, type-ahead.
  3. Selection, single or multiple, separate from checking.
  4. Tri-state checkboxes where a partly checked folder shows a dash.
  5. Lazy children that load on first expand, with a loading state and a way to retry.
  6. A filter that keeps each match's parents so you can see where it lives.
  7. Drag to move, plus a keyboard way to do the same move.

A phone adds two more: rows big enough to tap, and a row menu that opens as a bottom sheet instead of a tiny popover.

How do the React tree view libraries compare?

They split on three features: lazy children, drag to move and checkboxes. MUI X keeps lazy loading and reordering in its paid Pro package, react-arborist ships drag and drop and a filter but its README does not mention checkboxes, and shadcn/ui has no tree component at all, only a sidebar block. Facts below come from each project's public docs and README, as of October 2026.

OptionWhat it isCheckboxesLazy childrenDrag to moveFilter
react-arborist (opens in a new tab)MIT package, virtualized, needs react-dnd as a peer dependencyNot mentioned in the READMENot mentioned in the READMEDrag and drop sortingsearchTerm prop
MUI X Tree View (opens in a new tab)MIT Community package plus a commercial Pro packagecheckboxSelection with selectionPropagation (opens in a new tab)ProReordering is ProNot listed on the overview page
shadcn/ui sidebar-11 (opens in a new tab)A sidebar block "with a collapsible file tree"Not in the blockNot in the blockNot in the blockNot in the block
ReUI Tree (opens in a new tab)shadcn registry component on @headless-tree/reactShown in a permissions exampleA loading pattern in its FAQ, no exampleThrough dnd kit, per its FAQType-ahead only
Wingo UI Tree ViewPro source component, npx wingo-ui@latest add tree-viewTri-state with cascadeloadChildren with RetryMouse, long press and Alt + arrowssearchable with highlighted matches

If your app already runs on MUI, its free RichTreeView with checkbox selection is the shorter path. If you want a free virtualized tree and are fine styling it yourself, react-arborist is a good fit. People searching for a react tree view shadcn version usually want code they own in their repo: ReUI, shadcn-tree-view (opens in a new tab) (MIT, with drag and drop) and the Wingo UI Tree View all install as source. The rest of this guide covers the Wingo UI component.

What does the Tree View need to render?

One items array of nested nodes. Each node has an id that is unique across the whole tree, a label, and optional children.

bash
npx wingo-ui@latest add tree-view
tsx
"use client";
import { TreeView, type TreeNode } from "@/components/ui/tree-view";
const files: TreeNode[] = [
{
id: "app",
label: "app",
children: [
{ id: "layout", label: "layout.tsx" },
{ id: "page", label: "page.tsx" },
],
},
{ id: "components", label: "components", children: [{ id: "button", label: "button.tsx" }] },
{ id: "readme", label: "README.md" },
];
export function ProjectFiles({ onOpen }: { onOpen: (id: string) => void }) {
return (
<TreeView
items={files}
folderIcons
defaultExpandedKeys={["app"]}
onAction={(node) => onOpen(node.id)}
labels={{ tree: "Project files" }}
/>
);
}

That is a file tree React developers will recognize: folderIcons gives parents a folder icon that opens with them and gives leaves a file icon. onAction runs when a file gets Enter, a double click or a tap on a phone. Without it, a node with href follows its link. A node can also carry icon, badge, description, disabled and its own actions for the ⋯ menu, which is the Dropdown Menu entry shape.

Your data probably comes from a database as flat rows with a parentId. Convert it once:

ts
import type { TreeNode } from "@/components/ui/tree-view";
type Row = { id: string; parentId: string | null; name: string };
export function toTree(rows: Row[]): TreeNode[] {
const nodes = new Map<string, TreeNode>(rows.map((row) => [row.id, { id: row.id, label: row.name }]));
const roots: TreeNode[] = [];
for (const row of rows) {
const node = nodes.get(row.id)!;
const parent = row.parentId ? nodes.get(row.parentId) : undefined;
if (parent) (parent.children ??= []).push(node);
else roots.push(node);
}
return roots;
}

How do tri-state checkboxes work in a React tree view?

Turn on checkboxes and keep checkedKeys in state. With cascade on (the default), checking a folder checks every loaded, enabled node under it. A folder is checked when all its children are and shows a dash when only some are. Disabled rows are skipped, so "owner only" permissions stay unchecked when someone checks the whole group. The rows announce the state as aria-checked, with mixed for the dash.

A react tree view checkbox setup for a role editor looks like this:

tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { TreeView, flattenTree, type TreeNode } from "@/components/ui/tree-view";
export function RoleEditor({ permissions }: { permissions: TreeNode[] }) {
const [checked, setChecked] = useState<string[]>([]);
// with cascade, fully checked folders are in the keys too; save only the leaves
const leafIds = new Set(flattenTree(permissions).filter((row) => !row.hasChildren).map((row) => row.id));
return (
<form action="/api/roles/sales-agent" method="post">
<TreeView
items={permissions}
checkboxes
selectionMode="none"
checkedKeys={checked}
onCheckedKeysChange={setChecked}
defaultExpandedKeys={["sales"]}
labels={{ tree: "Permissions" }}
/>
<input type="hidden" name="permissions" value={checked.filter((id) => leafIds.has(id)).join(",")} />
<Button type="submit">Save role</Button>
</form>
);
}

Checking and selecting are separate. The box checks, the row selects. With selectionMode="none", a click on a file row checks it too, while a click on a folder row only opens it, which is what you want on a permissions screen. To compute a folder's state on the server with the same rule, use the exported getCheckedState(node, checkedKeys).

In the demo below, check the Invoices 2026 folder, then uncheck one of its files and watch the folder turn to a dash. Type in the filter field to narrow the tree.

Check Invoices 2026 to check its three files, uncheck one to see the dash, then type contract in the filter: the path to each match opens
$ npx wingo-ui@latest add tree-view
ProTree View docs

How do I load children lazily?

Mark a node with hasChildren and leave out children. On first expand, the tree calls loadChildren(node) and shows a spinner row. If the promise rejects, a Retry row takes its place. Results are cached inside the tree.

tsx
"use client";
import { TreeView, type TreeNode } from "@/components/ui/tree-view";
const roots: TreeNode[] = [
{ id: "drive", label: "Shared drive", hasChildren: true },
{ id: "archive", label: "Archive", hasChildren: true },
];
async function loadChildren(node: TreeNode): Promise<TreeNode[]> {
const response = await fetch(`/api/folders/${node.id}/children`);
if (!response.ok) throw new Error("Could not load");
return (await response.json()) as TreeNode[];
}
export function SharedDrive() {
return <TreeView items={roots} folderIcons loadChildren={loadChildren} labels={{ tree: "Shared drive" }} />;
}

Subfolders you have not fetched yet can come back with hasChildren: true. One limit to plan for: the filter only sees loaded nodes. If most of the tree is still unloaded, search on the server and show the results instead.

How does the filter work?

searchable adds a filter field above the tree. Matches keep their parents, the path to each one opens, and the matched letters turn bold. Case and diacritics never matter, so "skoda" finds Škoda. Typing is debounced by 150ms. ArrowDown moves from the field into the tree and Escape clears it. On a phone the field uses 16px text, so iOS Safari does not zoom in when it gets focus.

You can control the text with filter and onFilterChange, for example to keep it in the URL. A node whose label is JSX needs a textValue, or the filter and type-ahead have nothing to match against.

How do I add drag and drop to a React tree view?

Turn on draggable and handle onMove. A line shows where the row will land: before or after a row, or a ring around a folder it goes into. Hovering a closed folder opens it after a short pause, and dropping a folder into itself is refused. On touch, a long press of 250ms lifts the row so a swipe still scrolls the page; the dnd kit mobile guide explains why that delay matters.

That covers react tree view drag and drop with a pointer. The keyboard path is Alt + ArrowUp and Alt + ArrowDown to move the focused row among its siblings, Alt + ArrowRight to move it into the folder above, and Alt + ArrowLeft to move it out one level. Every move is announced to screen readers.

onMove receives { id, targetId, position, fromParentId, toParentId, index }, where index is the position among the new siblings. moveTreeNode applies it to your data and keeps untouched branches as they were:

tsx
"use client";
import { useState } from "react";
import { TreeView, moveTreeNode, type TreeMoveEvent, type TreeNode } from "@/components/ui/tree-view";
export function Documents({ initial }: { initial: TreeNode[] }) {
const [items, setItems] = useState(initial);
const onMove = (event: TreeMoveEvent) => {
const previous = items;
setItems(moveTreeNode(items, event));
fetch(`/api/nodes/${event.id}/move`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parentId: event.toParentId, index: event.index }),
})
.then((response) => {
if (!response.ok) setItems(previous);
})
.catch(() => setItems(previous));
};
return <TreeView items={items} draggable folderIcons onMove={onMove} labels={{ tree: "Documents" }} />;
}

Per node, draggable: false pins a row in place and droppable: false stops anything from landing inside it. droppable: true on a leaf lets it accept drops like a folder.

Drag a file into another folder, or focus a row and press Alt + ArrowUp to move it among its siblings
$ npx wingo-ui@latest add tree-view
ProTree View docs

Which keyboard shortcuts does a tree view need?

A tree view needs the WAI-ARIA tree keys: arrows to move and open, Home and End, Enter and Space, and type-ahead, all behind one Tab stop. The tree has role="tree", rows are treeitems and open folders hold a group. Inside it:

  • ArrowDown / ArrowUp move between visible rows; Home / End go to the first and last; PageUp / PageDown jump ten rows.
  • ArrowRight opens a closed folder or steps into an open one; ArrowLeft closes an open folder or jumps to the parent.
  • Enter opens a file or toggles a folder; Space selects, or checks when checkboxes are on.
  • Shift + arrows extend a multiple selection, Ctrl + A (Cmd + A on a Mac) selects every visible row, Escape clears it.
  • * opens every folder beside the focused one, and typing letters jumps to the next row that starts with them.
  • Shift + F10 or the ContextMenu key opens the row's ⋯ menu.

These follow the WAI-ARIA pattern, which recommends type-ahead for every tree and especially for trees with more than seven root nodes. All strings, including the move announcements, live in labels and merge over DEFAULT_TREE_VIEW_LABELS, so you can translate the tree without forking it.

How does it behave on a phone and with big trees?

On a touch screen rows grow to 44px, the whole row is the tap target, a tap on a file runs onAction, and the ⋯ button stays visible and opens a bottom sheet.

For big trees, virtualize="auto" renders only the rows in view once more than 300 rows are visible. A virtualized tree is flat in the DOM, sets aria-level, aria-setsize and aria-posinset on each row, skips the expand animation and defaults to a 420px scroll box. Set maxHeight to choose your own.

What are the common tree view pitfalls?

  • Duplicate ids. Selection, expansion, checks and moves all use id, so it must be unique across the whole tree, not only among siblings. A development warning names duplicates.
  • Saving folder ids from cascade. Fully checked folders are part of checkedKeys. Store leaves, as in the role editor above, or your backend has to know which ids are folders and what they contained when they were saved.
  • Moving without updating items. The tree is controlled by your data. If onMove does not update items, the row snaps back.
  • Using a tree for two levels. A list of sections with a few items each reads better as an Accordion or a Sidebar group.

When should I use the File Manager instead?

When the tree is the side panel of a documents screen. The File Manager takes a flat items list of { id, kind, name, parentId } and shows folders and files in a grid or a list, with a folder tree (built on the Tree View) beside them once the manager is 768px wide. It adds uploads with progress, rename in place, Move to, bulk actions and search across every folder. Its buildFolderTree turns the flat list into Tree View nodes, the same job as the toTree helper above.

Double click a folder to open it, then drag a file onto another folder or a breadcrumb to move it; right-click a file or use its ⋯ button for Rename and Move to
$ npx wingo-ui@latest add file-manager
ProFile manager docs

How do I add it to my project?

Run npx wingo-ui@latest add tree-view (or file-manager). The CLI copies the source into components/ui, adds the components and hooks it imports, and installs @dnd-kit/core, @tanstack/react-virtual, motion and lucide-react. Both are Pro components (pricing); the Checkbox they use is free. For more single-component deep dives, see the component guides category and the React form components guide, and if your tree is really a JSON payload, the React JSON viewer guide covers that case.

Components in this post

  • Tree View

    A React tree view for folders, categories or permissions: animated expand, selection, tri-state checkboxes, filter and drag to move.

    Pro
  • File manager

    A React file manager for the documents of a company or a record: folders and files in a grid or a list, a folder tree, uploads and bulk actions.

    Pro

FAQ

Does shadcn/ui have a tree view component?

No, as of October 2026. shadcn/ui ships a sidebar block with a collapsible file tree (sidebar-11) built from Collapsible, but no tree component with keyboard navigation, checkboxes or drag and drop. Community registries such as ReUI and mrlightful's shadcn-tree-view, and the Wingo UI Tree View, fill that gap.

How do I add checkboxes to a React tree view?

Turn on checkboxes and keep checkedKeys in state. With cascade on (the default), checking a folder checks everything under it, and a folder with only some children checked shows a dash, the indeterminate state.

How do I load tree children lazily in React?

Mark a node with hasChildren and leave out children, then pass a loadChildren function that returns a promise of the child nodes. The Tree View shows a spinner row while it loads, a Retry row if the promise rejects, and caches the result.

How do I convert a flat list with parentId into a tree?

Build a map from id to node in one pass, then push each node into its parent's children or into the root list. The helper in this post does it in about ten lines, and the File Manager's buildFolderTree does the same for its folders.

Is the Wingo UI Tree View free?

No. The Tree View and the File Manager are Pro components. The Checkbox and Dropdown Menu they build on are free.

  • Tree view
  • Checkboxes
  • Drag and drop
  • Accessibility
  • React

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Component guides

Embla Carousel React: Accessible Slides, No Layout Shift

Build an embla carousel react component for photos, listings, testimonials and onboarding, with keyboard controls, pausable autoplay and no layout shift.

SRSerban Rusu·Oct 19, 2026·9 min read
Component guides

React Animated Number Counter: Rolling Odometer Digits

Build a React animated number counter that rolls each digit like an odometer, takes its separators from Intl, holds its width and respects reduced motion.

SRSerban Rusu·Oct 19, 2026·9 min read
Component guides

React Audio Player with Waveform: Scrub, Seek, Peaks

Build a React audio player with waveform scrubbing for podcasts, chat voice notes and call logs, with keyboard seek, precomputed peaks and no layout shift.

SRSerban Rusu·Oct 19, 2026·9 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI