Compare
react-virtual-checkbox-tree vs MUI X Rich Tree View
MUI X gives you tri-state checkbox selection for free, in the MIT community package, and it is genuinely good. What it puts behind the Pro licence is virtualization — $299 per developer per year, which for a team of six is $1,794 a year to stop rendering rows you cannot see.
People assume the split runs the other way, so it is worth saying plainly: checkbox selection and parent/child propagation are in the free package. checkboxSelection plus selectionPropagation={{ descendants: true, parents: true }} gets you cascading selection with an automatic indeterminate state, at no cost, from a library with a full-time team behind it. If your tree is a few hundred items and you are already on Material UI, that is the correct answer and this page is not trying to change your mind.
The split is virtualization. That lives in RichTreeViewPro, from @mui/x-tree-view-pro, which needs an MUI X Pro licence.
Verified facts
- Community version
- @mui/x-tree-view 9.13.0, published 2026-09-04npm
- Weekly downloads
- 1,043,769npm registry
- Tri-state, community tier
- Yes — checkboxSelection + selectionPropagationMUI docs
- Virtualization
- RichTreeViewPro only, on by default in v9MUI docs
- Pro licence
- $299 per developer per yearMUI pricing
- Bundle
- 21.2 kB gzipped community / 40.8 kB Pro, before @mui/materialBundlephobia
- Virtualization request
- Issue #9685, closed 2026-01-23#9685
- Last verified
- 2026-09-10
What MUI X does better
It is a commercial product with a team, a release train and a support contract. That shows.
Ground this library does not hold
- Checkable folders that survive the round trip. With
selectionPropagation.parents, selecting every child selects the parent, and the parent’s ID comes back inselectedItems. This library’sonCheckreturns leaf IDs and nothing else, by design. If your API wants “the whole folder” as a value, MUI expresses it and this does not. - Label editing. The Rich Tree View has built-in item label editing. There is no equivalent here.
- Pro: lazy loading and item reordering. Async children on expand and drag-to-reorder, both supported, both documented, both absent here at any price.
- A design system that already matches your app. If the rest of the product is Material, the tree looks right on day one. Here you write every pixel — which is the selling point and also the work.
- Documentation, i18n, and a support channel. Localization, an accessibility story maintained by people whose job it is, and Pro support if something breaks in production.
- A million downloads a week against eight. Whatever you are about to hit, someone hit it, filed it, and it has a milestone.
What the $299 actually buys, and what it doesn’t
The Pro tier is not a rip-off — you are also getting lazy loading and reordering, and MUI X Pro covers the Data Grid too. The question is narrower: if virtualization is the only Pro feature you need, you are paying a per-seat annual fee for a rendering strategy. This library does that part for free and under MIT, at 5.6 kB gzipped (12.6 kB with its one dependency) against 40.8 kB for @mui/x-tree-view-pro before @mui/material and Emotion are counted.
You give up the design system, the editing, the reordering and the support contract to get it. That is the trade in one sentence.
// MUI X: tri-state is free. Virtualization is RichTreeViewPro.
// npm i @mui/x-tree-view @mui/material @emotion/react @emotion/styled
import { RichTreeView } from "@mui/x-tree-view/RichTreeView";
import { useState } from "react";
const items = [
{ id: "docs", label: "docs", children: [{ id: "readme", label: "README.md" }, { id: "guide", label: "guide.md" }] },
{
id: "src",
label: "src",
children: [
{ id: "engine", label: "engine.ts" },
{ id: "ui", label: "ui", children: [{ id: "tree", label: "tree.tsx" }, { id: "row", label: "row.tsx" }] },
],
},
];
export default function App() {
const [selected, setSelected] = useState<string[]>([]);
return (
<RichTreeView
checkboxSelection
items={items}
multiSelect
onSelectedItemsChange={(_event, ids) => setSelected(ids as string[])}
selectedItems={selected}
selectionPropagation={{ descendants: true, parents: true }}
/>
);
}// react-virtual-checkbox-tree: virtualized by default, no design system attached.
// npm i react-virtual-checkbox-tree
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}
/>
);
}Can I keep the Material look?
Yes — that is what renderCheckbox is for. Render @mui/material’s own Checkbox inside this tree and you get Material pixels with virtualized rows. The one rule you must not break is spreading a11yProps onto the control.
// Keep the MUI look, drop the MUI tree: render their Checkbox in renderCheckbox.
import Checkbox from "@mui/material/Checkbox";
import { CheckedState, 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() {
return (
<Tree
aria-label="Project files"
data={data}
estimateSize={36}
height={320}
renderCheckbox={({ a11yProps, checkedState }) => (
// a11yProps is { "aria-hidden": true, tabIndex: -1 } and MUST be spread:
// the ROW carries role="treeitem" and aria-checked, so the visual control
// has to stay out of the tab order and out of the accessibility tree.
<Checkbox
{...a11yProps}
checked={checkedState === CheckedState.Checked}
disableRipple
indeterminate={checkedState === CheckedState.Indeterminate}
onChange={() => {
/* the row owns the interaction; this keeps React from warning */
}}
size="small"
/>
)}
/>
);
}a11yProps is { "aria-hidden": true, tabIndex: -1 }. The row carries role="treeitem" and aria-checked, so a visible, focusable checkbox inside it makes every row announce twice and puts hundreds of controls in the tab order. Spread it. Same rule for renderExpander. Full details in Render props.
One typing detail: checkedState is the CheckedState string enum, not a bare string. Compare against CheckedState.Checked and CheckedState.Indeterminate — TypeScript rejects checkedState === "checked" even though the runtime value is exactly that string, which is also what lands in data-state on the row.
Where the difference stops being about money
Above roughly 50,000 nodes the comparison changes character. This library stores selection as sparse assignments rather than a list of selected IDs, so checking a folder of 50,000 leaves is one map write — measured at 1–3 µs at every tree size from 1,110 to 1,111,110 nodes. Any component whose public contract is “here is the array of selected items” has to build that array on every change; here you can opt out of building it and read getCheckedSubtrees() instead. That is a documented, measured difference, not a claim about MUI’s internals.
Use MUI X Rich Tree View if
Your app is already Material UI, your tree is in the hundreds or low thousands, or you need label editing, lazy loading or item reordering. The community tier's tri-state selection is free and good, and if you are buying MUI X Pro for the Data Grid anyway, the virtualized tree comes with it at no extra cost.
Use this one if
You need virtualization without a per-seat licence, you are not on Material UI, you want the markup to be yours, or your tree is large enough that materializing the selected-IDs array on every click is itself the bottleneck.
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.
- react-arboristVirtualized and excellent, but has no checkboxes.
- headless-treeSame philosophy, wider scope — and virtualization is your job.
- rc-tree / antd TreeGenuinely virtualized, if you want an Ant-shaped API.
- Writing it yourselfThe real competitor. The four bugs you will write.