Solution
Live nested checkboxes demo on CodeSandbox
HTML
<div id="tree"></div>
Javascript
class NestedCheckboxes {
constructor(data, container) {
this.data = data;
this.container = container;
this.render();
}
setAllChildren(node, checked) {
node.checked = checked;
if (node.children) node.children.forEach(c => this.setAllChildren(c, checked));
}
updateParentState(node) {
if (!node.children || node.children.length === 0) return;
node.children.forEach(c => this.updateParentState(c));
const total = node.children.length;
const checkedCount = node.children.filter(c => c.checked === true).length;
const indeterminateCount = node.children.filter(c => c.checked === 'indeterminate').length;
if (checkedCount === total) {
node.checked = true;
} else if (checkedCount === 0 && indeterminateCount === 0) {
node.checked = false;
} else {
node.checked = 'indeterminate';
}
}
applyToCheckbox(el, state) {
if (state === 'indeterminate') {
el.checked = false;
el.indeterminate = true;
} else {
el.indeterminate = false;
el.checked = !!state;
}
}
buildTree(nodes, container, depth = 0) {
nodes.forEach(node => {
const wrapper = document.createElement('div');
wrapper.style.marginLeft = `${depth * 24}px`;
const label = document.createElement('label');
label.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer;';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.id = `cb-${node.id}`;
this.applyToCheckbox(cb, node.checked);
cb.addEventListener('change', () => {
this.setAllChildren(node, cb.checked);
this.update();
});
const lbl = document.createElement('span');
lbl.textContent = node.label;
label.appendChild(cb);
label.appendChild(lbl);
wrapper.appendChild(label);
if (node.children && node.children.length) {
this.buildTree(node.children, wrapper, 1);
}
container.appendChild(wrapper);
});
}
update() {
this.data.forEach(node => this.updateParentState(node));
this.sync(this.data);
}
sync(nodes) {
nodes.forEach(node => {
const cb = document.getElementById(`cb-${node.id}`);
if (cb) this.applyToCheckbox(cb, node.checked);
if (node.children) this.sync(node.children);
});
}
render() {
this.container.innerHTML = '';
this.buildTree(this.data, this.container);
}
}
Usage
const data = [
{
id: 1, label: "Fruits", checked: false,
children: [
{ id: 2, label: "Apple", checked: false },
{ id: 3, label: "Banana", checked: false },
{
id: 4, label: "Citrus", checked: false,
children: [
{ id: 5, label: "Orange", checked: false },
{ id: 6, label: "Lemon", checked: false },
],
},
],
},
{
id: 7, label: "Vegetables", checked: false,
children: [
{ id: 8, label: "Tomato", checked: false },
{ id: 9, label: "Cucumber", checked: false },
],
},
];
const tree = new NestedCheckboxes(data, document.getElementById('tree'));