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'));