promethys/checkbox-tree
Render hierarchical checkbox trees in Laravel Blade with automatic parent-child selection, indeterminate states, and expandable nodes—ideal for permissions, categories, and nested filters.
promethys/checkbox-tree (now tagged 1.0.0, the first stable release) is a form-oriented package: it renders a nested set of checkboxes from hierarchical data and gives you back the checked IDs on submit. The 1.0.0 tag itself ships with no changelog body beyond a commit link, so treat the tag as "API declared stable" rather than "features documented" — skim the commit range before assuming behaviour.
Minimal steps:
composer require promethys/checkbox-tree
php artisan vendor:publish --provider="Promethys\CheckboxTree\CheckboxTreeServiceProvider"
The publish step typically exposes the config (attribute names for id/label/children, whether parent nodes are selectable, asset registration) and the Blade views used to render each node. Read the published config first — nearly every behavioural question ("why is my parent checkbox disabled?", "why is the label column wrong?") is answered there rather than in the package source.
First use case to build: a category tree on an edit form. Load your adjacency-list model, build the nested array, hand it to the tree renderer with the model's currently-attached IDs, and validate the submitted array in a FormRequest. If that round-trip works, everything else is variation.
Where to look, in order:
config/checkbox-tree.php — naming keys, asset paths, default view.Feeding the tree. Most usage builds a nested structure from an adjacency list (parent_id) or from a nested-set/closure-table package. Do the tree assembly yourself (or via ->toTree() on your model library) and pass the roots in; keep ordering explicit (orderBy('position')) before nesting, because the renderer preserves the order it receives and there is no sorting step in the view.
Checked state. Pass an array of IDs that should start checked. On submit, read back a flat array of IDs and sync:
$validated = $request->validate([
'nodes' => ['array'],
'nodes.*' => ['integer', 'exists:categories,id'],
]);
$category->categories()->sync($validated['nodes'] ?? []);
Validate with exists — the client can post anything, including IDs from a tree the user was never shown.
Blade integration. The common pattern is a form partial that takes $tree, $checked, and a name prefix, rendered inside your existing form. If the package exposes a per-node view config key, override it once in config/checkbox-tree.php rather than copy-pasting markup into every form.
Per-node customization. For anything richer than label-plus-checkbox (icons, counts, "selected 3 of 7 descendants" badges), use the per-node view override and keep the input markup identical — the package's JS hooks the input by name/id, so changing those attributes silently breaks cascading.
Livewire / Inertia. With Livewire, bind the flat checked array to a public property and re-render the tree on change; make sure the tree data itself is a computed property, not rebuilt per request in the view. With Inertia, the package's Blade rendering doesn't apply — you'll be reimplementing the cascade in Vue/React, so consider it only if you also need the JS behaviour elsewhere.
Seeding and defaults. For "select all" defaults, expand parents server-side into descendant IDs and pass the full flat list as $checked; don't rely on the client to cascade a single parent ID downward.
old('nodes', $model->pluck('id')), not just the model, or the user loses their selection on a failed submit. This is the single most common complaint against tree-checkbox widgets.get() + in-memory nesting. Recursive lazy loading on a 5-level tree is a measurable page-load hit.public/vendor/... is not refreshed by composer update. Re-run vendor:publish --force (or your Vite pipeline) after any version change, then hard-refresh — otherwise you'll debug the old cascade logic.name keys (nodes[] vs permissions[]) and, if the JS binds by selector, distinct container classes. Verify the config's selector option before duplicating a tree.dd($tree) immediately before the render call, and dd($request->all()) on submit. Nine times out of ten the bug is the array shape (children key name, or a mixed string/int ID set — in_array comparisons then fail because the checked list came back as strings from the request).0.x/dev-main constraint, diff the published config and the views against your current copies before bumping — config key renames and view restructuring are the usual 1.0 breaking changes, and both fail quietly (tree renders empty, or checks don't restore) rather than throwing. Pin ^1.0 in composer.json and treat the next minor as potentially behaviour-changing until a real changelog appears.How can I help you explore Laravel packages today?