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

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:
- Expand and collapse with an animation that does not jump the page.
- 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.
- Selection, single or multiple, separate from checking.
- Tri-state checkboxes where a partly checked folder shows a dash.
- Lazy children that load on first expand, with a loading state and a way to retry.
- A filter that keeps each match's parents so you can see where it lives.
- 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.
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.
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:
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:
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.
$ npx wingo-ui@latest add tree-viewHow 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.
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:
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.
$ npx wingo-ui@latest add tree-viewWhich 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. IfonMovedoes not updateitems, 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.
$ npx wingo-ui@latest add file-managerHow 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.
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


