Compare
react-virtual-checkbox-tree vs react-checkbox-tree
react-checkbox-tree renders a DOM node for every node in your tree. That is the whole difference, and for most trees it does not matter. It matters at about five thousand nodes, and it is fatal at fifty thousand — which is why issue #43 has been open since July 2017.
These two libraries have the same mental model: a tree of nodes, leaf-only checked values, a parent that goes indeterminate when its children disagree, controlled checked and expanded arrays. If you already know react-checkbox-tree you already know this one, and the migration guide is a prop-by-prop table rather than a rewrite.
What differs is everything below the API: rows are virtualized, the cascade is stored sparsely instead of as an array of every checked value, there is a real role="tree" with arrow-key navigation, and there is no stylesheet.
Verified facts
- react-checkbox-tree version
- 2.0.2, published 2026-05-28npm
- Weekly downloads
- 61,054npm registry
- GitHub stars
- 731repo
- Virtualization
- None. Issue #43 open since 2017-07-17#43
- Keyboard navigation
- Space / Enter only. #24 open since 2017-03-04#24
- Screen reader support
- #280 open since 2021-05-22#280
- Bundle
- 11.0 kB gzipped + a required stylesheetBundlephobia
- Licence
- MIT, same as this library
- Last verified
- 2026-09-10
What react-checkbox-tree does better
Quite a lot, and some of it is not on any roadmap here.
Ground this library does not hold
- Checkable folders.
checkModel="all"puts parent values into thecheckedarray alongside leaves. This library cannot do that at all —onCheckreturns leaf IDs and only leaf IDs, forever, because a folder’s state here is derived rather than stored. If your server needs to hear “the wholesrcfolder” rather than “these five files”, react-checkbox-tree expresses that today and this one does not. - Independent parent and child state.
noCascadegives you a tree where checking a parent does nothing to its children. There is no equivalent prop here; the cascade is the library. - It works as a plain form field. The
nameandnameAsArrayprops render a hidden<input>, so a server-rendered form posts the selection with no JavaScript state at all. - Right-to-left and localization. A
directionprop and alangobject for every string it renders. This library renders no strings of its own, which sounds like an answer but means you build both yourself. - Per-node disabling, icons and titles.
disabled,icon,showCheckboxandtitleper node, plusonContextMenu. Here you would reach forrenderItemand wire those yourself. - Ten years and 61,000 downloads a week. It shipped in February 2016. It has been through React 16, 17, 18 and 19. Whatever edge case you are about to hit, somebody hit it first and there is probably an issue about it. This library has eight downloads a week and a 0.x version number.
What changes if you switch
The data goes flat
The single real migration cost. react-checkbox-tree takes a nested array of { value, label, children }; this takes a flat map keyed by ID with a __root__ entry. Lookups become O(1), IDs stay stable across updates, and you can build the map straight out of a SQL result — but you do have to convert.
// npm i react-checkbox-tree
import { useState } from "react";
import CheckboxTree from "react-checkbox-tree";
import "react-checkbox-tree/lib/react-checkbox-tree.css";
const nodes = [
{
value: "docs",
label: "docs",
children: [
{ value: "readme", label: "README.md" },
{ value: "guide", label: "guide.md" },
],
},
{
value: "src",
label: "src",
children: [
{ value: "engine", label: "engine.ts" },
{
value: "ui",
label: "ui",
children: [
{ value: "tree", label: "tree.tsx" },
{ value: "row", label: "row.tsx" },
],
},
],
},
];
export default function App() {
const [checked, setChecked] = useState<string[]>([]);
const [expanded, setExpanded] = useState<string[]>(["src"]);
return (
<CheckboxTree
checked={checked}
expanded={expanded}
nodes={nodes}
onCheck={setChecked}
onExpand={setExpanded}
/>
);
}// npm i react-virtual-checkbox-tree
import { useState } from "react";
import { Tree, type TreeDefinition } from "react-virtual-checkbox-tree";
// Same tree, flat instead of nested. Keys are IDs; "__root__" holds the top level.
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[]>([]);
const [expanded, setExpanded] = useState<string[]>(["src"]);
return (
<Tree
aria-label="Project files"
checkedItems={checked}
data={data}
expandedItems={expanded}
height={320}
onCheck={setChecked}
onExpand={setExpanded}
/>
);
}If your data arrives nested from an API, convert it once:
// Nested -> flat, once, at the edge of your app.
import type { TreeDefinition, TreeItem } from "react-virtual-checkbox-tree";
type LegacyNode = { children?: LegacyNode[]; label: string; value: string };
export function flatten(nodes: LegacyNode[]): TreeDefinition {
const data: TreeDefinition = {
__root__: { id: "__root__", label: "root", children: nodes.map((n) => n.value) },
};
const walk = (node: LegacyNode) => {
const item: TreeItem = { id: node.value, label: node.label };
if (node.children?.length) {
item.children = node.children.map((child) => child.value);
node.children.forEach(walk);
}
data[node.value] = item;
};
nodes.forEach(walk);
return data;
}The stylesheet goes away
There is no react-virtual-checkbox-tree/lib/*.css to import, because the library ships no stylesheet. The only styles it sets are the inline ones virtualization requires — the absolutely positioned row, its height, and the indent. Everything visual is yours. Rows carry data-rvct-row, data-state ("checked" / "unchecked" / "indeterminate"), data-level, data-expanded, data-leaf and data-active, and you style those. If you liked the Font Awesome look you will have to rebuild it; see Styling.
onCheck changes shape
react-checkbox-tree calls onCheck(checked, targetNode). This calls onCheck(checkedLeafIds) — one argument, always leaf IDs, never folders. If you were using checkModel="all" there is no equivalent and you will need to derive the folder set yourself from the leaf IDs.
Keyboard and screen readers start working
react-checkbox-tree toggles on Space and Enter via its checkKeys prop, and that is the extent of it: there are no arrow keys, and issue #280 (“Checkbox tree is not accessible through keyboard”) has been open since 2021. This library implements the WAI-ARIA tree pattern: role="tree" with aria-multiselectable, role="treeitem" with aria-level, aria-setsize, aria-posinset and aria-checked="mixed", plus ArrowUp/Down/Left/Right, Home, End, Space, Enter, * to expand everything, Ctrl/Cmd+A, and type-ahead. Focus is tracked with aria-activedescendant on the container, so a row being unmounted by the virtualizer can never take the focus with it.
How much does the virtualization actually buy?
Below roughly 2,000 nodes: nothing you will notice. react-checkbox-tree flattens its source tree into a map internally and memoizes, and it is not a slow library — it is an unvirtualized one, which is a different problem. The cost is DOM: every node is an element, every check re-renders the subtree, and every value lands in a checked array that grows with the selection.
Here the DOM holds a window of rows regardless of tree size, and the selection is stored as sparse assignments rather than a list — checking a folder of 50,000 leaves writes one map entry. These are measured, on the published build, with npm run bench:
| Nodes | Cascade a check | Read the selection | Rows in the DOM |
|---|---|---|---|
| 1,110 | 3 µs | 53 µs | ~20 |
| 11,110 | 1 µs | 320 µs | ~20 |
| 111,110 | 1 µs | 3.7 ms | ~20 |
The honest column is the middle one. Asking for the full list of checked leaf IDs has to materialize that list, and at 111,110 nodes that is 3.7 ms per change. At that scale you read getCheckedSubtrees() instead — the sparse form — which is what Persist a selection is about. Building the engine is also linear: 115 ms at 111,110 nodes, about two seconds at a million.
Should you migrate?
Probably not, if it currently works. A working react-checkbox-tree with 400 nodes in it is not a problem you have. Migrate when the tree got big enough that opening the panel janks, when an accessibility audit came back, or when you need search that filters rather than highlights.
Use react-checkbox-tree if
Your tree is a few thousand nodes or fewer, you want checkable folders or noCascade, you need RTL and localized strings out of the box, or you value ten years of production mileage over any of the above. It is a good library and the number of apps quietly relying on it is the strongest argument in its favour.
Use this one if
The tree is large enough that rendering every node hurts, you need arrow-key navigation and a correct ARIA tree, you want search that filters and a selection that survives it, or you would rather own the markup than override a stylesheet.
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-arboristVirtualized and excellent, but has no checkboxes.
- 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.