Compare
react-virtual-checkbox-tree vs react-arborist
react-arborist has no checkboxes. That is the whole comparison: it is a better file-manager tree than this will ever be, and it does not ship the one thing this library exists to do. Issue #352 — “Tracking: Checkbox selection & indeterminate (parent) state” — is open.
Both are virtualized. Both are TypeScript-first. Both let you render the row. They part company on purpose: react-arborist is built around manipulating a tree — drag to reorder, rename in place, create, delete — and its selection model is row selection, the kind you get from a file explorer. This library is built around choosing a set of leaves, and the tri-state cascade is the entire product.
If you are choosing between them and your requirement contains the word “drag”, stop reading and use react-arborist.
Verified facts
- react-arborist version
- 3.16.0, published 2026-07-25npm
- Weekly downloads
- 433,646npm registry
- GitHub stars
- 3,704repo
- Checkboxes / indeterminate
- Not built in — #352 open since 2026-06-07#352
- Virtualization
- Yes, via react-windowrepo
- Bundle
- 30.8 kB gzipped, 5 dependenciesBundlephobia
- Licence
- MIT, same as this library
- Last verified
- 2026-09-10
What react-arborist does better
Nearly everything that is not a checkbox.
Ground this library does not hold
- Drag and drop, properly. Reordering, dropping into folders, a drop cursor you can replace with
renderCursor, a drag preview you can replace withrenderDragPreview. This library has none of it and it is an explicit non-goal, not a to-do. - Inline renaming and CRUD.
onCreate,onRename,onMove,onDeleteand an editing state on every node. That is a file manager in a box. Here you would build all of it around the tree. - Range selection. Shift-click and shift-arrow select a contiguous run of rows. This library toggles one row at a time with Space and offers Ctrl/Cmd+A for everything — there is no range selection.
- A richer node API.
NodeApigives younode.toggle(),node.edit(),node.select(),node.isEditing,node.nextSiblingand the rest, plus aTreeApifor the whole thing. The equivalent here is the Engine, which is deliberately smaller. - Fifty times the install base. 433,646 downloads a week and 3,704 stars against eight and a handful. Its edge cases have been found.
Can I just add checkboxes to react-arborist?
You can render one, and then you own the tri-state logic. That is more than it sounds. The maintainer has consolidated four separate requests — #312, #173, #267 and #190 — into a single tracking issue #352, which is the clearest possible signal that people keep wanting this and it is not there yet.
Here is the honest shape of the DIY version next to the built-in one. The left column is a complete, working tri-state cascade — and it is still missing the ARIA tree the right column emits (role="treeitem" with aria-checked="mixed", aria-level, aria-setsize and aria-posinset), and leavesUnder() walks the entire subtree on every render of every row.
// react-arborist: the checkbox is yours to build, and so is the cascade.
import { useCallback, useState } from "react";
import { Tree, type NodeApi } from "react-arborist";
type Item = { children?: Item[]; id: string; name: string };
const data: Item[] = [
{ id: "docs", name: "docs", children: [{ id: "readme", name: "README.md" }, { id: "guide", name: "guide.md" }] },
{
id: "src",
name: "src",
children: [
{ id: "engine", name: "engine.ts" },
{ id: "ui", name: "ui", children: [{ id: "tree", name: "tree.tsx" }, { id: "row", name: "row.tsx" }] },
],
},
];
/** You write this. It is the part that goes wrong at depth 3. */
function leavesUnder(node: NodeApi<Item>): string[] {
if (node.isLeaf) return [node.id];
return (node.children ?? []).flatMap(leavesUnder);
}
function stateOf(node: NodeApi<Item>, checked: Set<string>) {
const leaves = leavesUnder(node); // walks the whole subtree, on every render
const n = leaves.filter((id) => checked.has(id)).length;
if (n === 0) return "unchecked";
return n === leaves.length ? "checked" : "indeterminate";
}
export default function App() {
const [checked, setChecked] = useState<Set<string>>(new Set());
const toggle = useCallback((node: NodeApi<Item>, next: boolean) => {
setChecked((prev) => {
const out = new Set(prev);
for (const id of leavesUnder(node)) {
if (next) out.add(id);
else out.delete(id);
}
return out;
});
}, []);
return (
<Tree data={data} height={320} rowHeight={32} width={320}>
{({ node, style }) => {
const state = stateOf(node, checked);
return (
<div style={style}>
<input
aria-label={node.data.name}
checked={state === "checked"}
onChange={(event) => toggle(node, event.target.checked)}
ref={(el) => {
if (el) el.indeterminate = state === "indeterminate";
}}
type="checkbox"
/>
<span onClick={() => node.toggle()}>{node.data.name}</span>
</div>
);
}}
</Tree>
);
}// react-virtual-checkbox-tree: the cascade is the library.
import { useState } from "react";
import { Tree, type TreeDefinition } from "react-virtual-checkbox-tree";
const data: TreeDefinition = {
__root__: { id: "__root__", label: "root", children: ["docs", "src"] },
docs: { id: "docs", label: "docs", children: ["readme", "guide"] },
readme: { id: "readme", label: "README.md" },
guide: { id: "guide", label: "guide.md" },
src: { id: "src", label: "src", children: ["engine", "ui"] },
engine: { id: "engine", label: "engine.ts" },
ui: { id: "ui", label: "ui", children: ["tree", "row"] },
tree: { id: "tree", label: "tree.tsx" },
row: { id: "row", label: "row.tsx" },
};
export default function App() {
const [checked, setChecked] = useState<string[]>([]);
return (
<Tree
aria-label="Project files"
checkedItems={checked}
data={data}
estimateSize={32}
height={320}
onCheck={setChecked}
/>
);
}The scaling difference is not the line count, it is leavesUnder(). Recomputing a parent’s state by walking its descendants is O(subtree) per row per render. This library stores selection as sparse assignments and derives state with a memoized post-order walk that skips any branch containing no assignments, which is why a cascade costs 1–3 µs whether the tree holds a thousand nodes or a million. The mechanism is in Checkbox semantics.
Can I use both?
Yes, and it is a reasonable answer. The react-virtual-checkbox-tree/engine entry point ships the flattening, the tri-state math and the search filter as a plain class with no React and no virtualizer in it. You can drive react-arborist’s rendering and let the Engine own the checkbox state — engine.getViewState(id) per row, engine.toggle(id, next) on click, engine.subscribe() to re-render. See the engine-only example.
Use react-arborist if
You are building a file explorer, an outline editor, a layers panel, or anything where users move nodes around. Drag-and-drop, inline rename and range selection are all first class there and absent here — and its 433,000 weekly downloads mean the sharp edges are already filed off.
Use this one if
Selection is the point and manipulation is not: a file picker, a permissions matrix, a faceted filter, an export dialog. You want the indeterminate math, ancestor-aware search and an ARIA tree without writing the cascade — and you are fine having no drag-and-drop at all.
Every fact on this page was checked on 2026-09-10 and links to its source. Numbers move and libraries ship; if something here is out of date or unfair, open an issue and it gets fixed. Corrections that make a competitor look better are the most welcome kind.
Other comparisons
- react-checkbox-treeThe incumbent. Same mental model, no virtualization path.
- headless-treeSame philosophy, wider scope — and virtualization is your job.
- MUI X Rich Tree ViewVirtualization is behind the Pro licence.
- rc-tree / antd TreeGenuinely virtualized, if you want an Ant-shaped API.
- Writing it yourselfThe real competitor. The four bugs you will write.