1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Nested Checkbox

Nested Checkbox

ui

The checkbox list with parents and children that agree with each other: tick every child and the parent fills in, tick the parent and the whole branch follows, tick some of them and the parent shows the third state — a dash, announced as "partially checked" rather than as "not checked". Reach for it wherever a form hands out a hierarchy rather than a flat list: permissions, roles, scopes and API-token access on a settings or admin screen, category and facet filters on a storefront or search page, which columns or fields a CSV or report export includes, notification settings crossed with channels (email, push, Slack), per-purpose consent on a privacy or cookie screen, selecting files and folders for an upload, a download, a backup or a bulk move, choosing which locations, teams, projects or environments a rule applies to, picking a taxonomy or tag subtree, and the "select all" on a nested table of rows. Common asks it answers: "nested checkbox react", "parent child checkbox", "checkbox tree react", "tri-state checkbox", "three state checkbox", "indeterminate checkbox react", "half checked checkbox", "partially selected checkbox", "aria-checked mixed", "mixed state checkbox", "select all with children react", "check all children when parent is checked", "parent checkbox updates when children change", "permission tree component", "scopes checkbox tree", "category tree with checkboxes", "folder tree with checkboxes", "shadcn nested checkbox", "shadcn checkbox tree", "shadcn indeterminate checkbox", "radix checkbox indeterminate dash", "react-checkbox-tree alternative", "rc-tree checkable alternative", "MUI TreeView with checkboxes equivalent", "antd Tree checkable equivalent", "how to set indeterminate in react", "input indeterminate react no prop", "checkbox dash shows but screen reader says unchecked", "recursive checkbox state react", "nested checkbox three levels", "disabled child breaks select all", "親子チェックボックス react", "全選択 子要素 連動", "中間状態 チェックボックス", "indeterminate 実装". Official shadcn/ui has nothing for this, and the gap is narrower and more surprising than usual. Measured today over all 63 catalogue entries (62 are fetchable — questionnaire is listed and 404s on both style tracks) and 223,287 bytes of their source: indeterminate, mixed, aria-checked, partially, subtree and descendant are every one of them a zero hit, while checkbox itself appears 13 times. So the checkbox is there and the third state is not. Reading its source: the indicator holds a CheckIcon and nothing else — no dash, and no branch on which state it is in — and every colour rule on the box is keyed to data-[state=checked], so the indeterminate state the Radix primitive underneath it can represent has no appearance of its own in the wrapper at all. Composing on that component and passing the third state through is the obvious move and it does not produce a third state, which is why this one is self-contained. Within this registry the neighbours are close enough to be worth naming. tree-view is the ARIA tree pattern — one tab stop for the whole tree, arrow keys, rows that open and close: that is hierarchy you navigate, this is hierarchy you select, and the two are routinely wanted on the same screen. multi-select and tag-input put chips inside a single field and choose from a flat list. bulk-action-bar is about the rows already selected rather than the act of selecting them, and filter-chips shows which conditions are in force. Nothing here held a checkbox before this. Six things separate it from the thirty-line version, and five of them are invisible until somebody is already relying on it. First, and the reason it is not an <input type="checkbox">: the third state is a value, aria-checked="mixed", not a dash drawn in CSS. indeterminate is a DOM property with no HTML attribute behind it, so React cannot express it in JSX — it has to be assigned to the node through a ref after mount, which means the server render, the first paint, a snapshot test and any crawler all see aria-checked="false" on a row that is actually part-selected. That is the exact bug where the dash is on screen and a screen reader says "not checked", and it survives every visual review because the picture is right. Each row here is a role="checkbox" button, so the state is an attribute and the markup is true at every moment. Second, the roll-up runs in both directions or it is visibly broken. A parent's state comes up from its children and is never stored — keep a parent's own boolean beside the children's and you have two sources of truth that disagree the moment either side is touched — while a press on a parent goes back down and writes every leaf beneath it. Implement only the upward half and pressing a parent does nothing; only the downward half and the parent goes stale as soon as a child is pressed on its own. Third, it recurses. A parent of parents of leaves rolls up through every level, which is the part that looks finished at two levels deep and breaks the day somebody nests a third — and the state and the write are each one post-order pass over the tree, not a walk per row. Fourth, disabled rows, which is where the careful implementations still go wrong, because the question has two different answers. What a parent displays counts its disabled children: a group claiming "all of it" while a visibly empty row sits underneath is lying about what will be submitted. What a press changes excludes them: a locked permission must not be granted by pressing its group. Decide the direction of the press from the displayed state and a parent held at mixed by one disabled-and-off row can never clear itself — it is not "checked", so every press reads as "turn it all on" and the thing can be pressed forever without changing. The direction here comes from the leaves it may touch, so the group clears on the first press. Disabling a parent disables its whole subtree, and a row with nothing togglable beneath it reports itself as such instead of looking live and doing nothing. Fifth, ids it does not recognise are carried through untouched rather than rebuilt from what is on screen, so a tree that is filtered, paginated or lazily loaded does not quietly discard the choices made on another page — and the emitted array keeps the order it arrived in, appending additions in tree order, so it is stable across presses instead of reshuffling on every change. Sixth, the locked rows are aria-disabled rather than carrying the disabled attribute, so they stay focusable and somebody tabbing through can still discover that a permission exists and is held shut; a node's description is wired up with aria-describedby from outside the control, so it is announced after the name instead of swallowed into it; and each subtree is a role="group" named by the row above it, so entering it says which group these rows belong to. The value is the set of checked leaf ids — group ids never appear in it, because a derived state that is also saved is the drift described above — controlled with value plus onChange (handed the next ids and the node that was pressed) or uncontrolled with defaultValue. collectLeafIds, nodeCheckState and toggleCheckboxNode are exported as pure functions, so a check-all, an "is this whole branch on" test for a payload, and the roll-up itself are all usable headless. Each checkbox is its own tab stop the way a list of checkboxes in a form is, and Space toggles. Pass a name to submit the selection with a plain form POST as one hidden input per id. Styled entirely with shadcn tokens (primary, primary-foreground, input, background, ring, muted-foreground), so it follows light and dark mode, and lucide-react for the two icons is the only dependency — no Radix, no state library.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/nested-checkbox.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
  • lucide-react
    components/ui/nested-checkbox.tsx