Open demo

CSS code

HTML
<div><span class="lbl">Collapsible subtasks</span>
<section class="st" data-tree aria-labelledby="menu-title">
  <h2 id="menu-title">Launch the summer menu</h2>
  <p class="st-sub">Tick a task to finish all of its subtasks at once.</p>
  <ul class="st-tree">
    <li class="st-task" data-task>
      <div class="st-row"><button class="st-tog" type="button" aria-expanded="true" aria-controls="menu-1-subs" aria-label="Subtasks of “Photograph the new dishes”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="st-box" type="checkbox" id="menu-1" data-parent-box><label class="st-title" for="menu-1">Photograph the new dishes</label><span class="st-count" data-count>2 of 3</span></div>
      <ul class="st-subs" id="menu-1-subs" data-subtasks>
        <li><input class="st-box sm" type="checkbox" id="menu-1-1" checked><label for="menu-1-1">Book the photographer</label></li>
        <li><input class="st-box sm" type="checkbox" id="menu-1-2" checked><label for="menu-1-2">Prep six hero plates</label></li>
        <li><input class="st-box sm" type="checkbox" id="menu-1-3"><label for="menu-1-3">Pick the final twelve shots</label></li>
      </ul>
    </li>
    <li class="st-task" data-task>
      <div class="st-row"><button class="st-tog" type="button" aria-expanded="true" aria-controls="menu-2-subs" aria-label="Subtasks of “Print the menus”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="st-box" type="checkbox" id="menu-2" data-parent-box><label class="st-title" for="menu-2">Print the menus</label><span class="st-count" data-count>0 of 3</span></div>
      <ul class="st-subs" id="menu-2-subs" data-subtasks>
        <li><input class="st-box sm" type="checkbox" id="menu-2-1"><label for="menu-2-1">Proof the allergen codes</label></li>
        <li><input class="st-box sm" type="checkbox" id="menu-2-2"><label for="menu-2-2">Send the files to Pressworks</label></li>
        <li><input class="st-box sm" type="checkbox" id="menu-2-3"><label for="menu-2-3">Collect 200 copies</label></li>
      </ul>
    </li>
    <li class="st-task" data-task>
      <div class="st-row"><button class="st-tog" type="button" aria-expanded="false" aria-controls="menu-3-subs" aria-label="Subtasks of “Train the floor team”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="st-box" type="checkbox" id="menu-3" data-parent-box checked><label class="st-title" for="menu-3">Train the floor team</label><span class="st-count" data-count>2 of 2</span></div>
      <ul class="st-subs" id="menu-3-subs" data-subtasks>
        <li><input class="st-box sm" type="checkbox" id="menu-3-1" checked><label for="menu-3-1">Tasting session on Monday</label></li>
        <li><input class="st-box sm" type="checkbox" id="menu-3-2" checked><label for="menu-3-2">Hand out the wine pairing notes</label></li>
      </ul>
    </li>
  </ul>
  <p class="sr-only" role="status" data-live></p>
</section></div>

<div><span class="lbl">Two-level checklist</span>
<section class="st" data-tree aria-labelledby="rel-title">
  <h2 id="rel-title">Release 4.2</h2>
  <p class="st-sub">Planned for Wednesday 17 June</p>
  <ul class="st-ttree">
    <li class="st-ttask" data-task>
      <div class="st-trow"><input class="st-box" type="checkbox" id="rel-1" data-parent-box checked><label for="rel-1">Code freeze</label><span class="st-count" data-count>2 of 2</span></div>
      <ul class="st-tsubs" data-subtasks>
        <li><input class="st-box sm" type="checkbox" id="rel-1-1" checked><label for="rel-1-1">Merge the last feature branch</label></li>
        <li><input class="st-box sm" type="checkbox" id="rel-1-2" checked><label for="rel-1-2">Tag the release candidate</label></li>
      </ul>
    </li>
    <li class="st-ttask" data-task>
      <div class="st-trow"><input class="st-box" type="checkbox" id="rel-2" data-parent-box><label for="rel-2">QA sign-off</label><span class="st-count" data-count>1 of 3</span></div>
      <ul class="st-tsubs" data-subtasks>
        <li><input class="st-box sm" type="checkbox" id="rel-2-1" checked><label for="rel-2-1">Run the regression suite</label></li>
        <li><input class="st-box sm" type="checkbox" id="rel-2-2"><label for="rel-2-2">Test payments on staging</label></li>
        <li><input class="st-box sm" type="checkbox" id="rel-2-3"><label for="rel-2-3">Check the Android build</label></li>
      </ul>
    </li>
    <li class="st-ttask" data-task>
      <div class="st-trow"><input class="st-box" type="checkbox" id="rel-3" data-parent-box><label for="rel-3">Ship it</label><span class="st-count" data-count>0 of 2</span></div>
      <ul class="st-tsubs" data-subtasks>
        <li><input class="st-box sm" type="checkbox" id="rel-3-1"><label for="rel-3-1">Publish the release notes</label></li>
        <li><input class="st-box sm" type="checkbox" id="rel-3-2"><label for="rel-3-2">Post in #announcements</label></li>
      </ul>
    </li>
  </ul>
  <p class="sr-only" role="status" data-live></p>
</section></div>
CSS
/* Subtasks: every task and subtask is a real checkbox with a label. The script ties a parent to its subtasks,
   including the indeterminate state, and the chevron buttons are disclosure buttons with aria-expanded. */
.st{--st-ink:#2a0f1c;--st-muted:#6b4a58;--st-line:#f0dce4;--st-guide:#e3c3d0;--st-box:#9a7383;--st-accent:#be185d;--st-accent-dark:#9d174d;--st-tint:#fce7ef;
  background:#fff;color:var(--st-ink);border:1px solid var(--st-line);border-radius:18px;padding:24px 24px 18px;box-shadow:0 18px 40px -28px rgba(42,15,28,.4)}
.st [hidden]{display:none!important}
.st :where(h2,p,ul){margin:0;padding:0}
.st ul{list-style:none}
.st h2{font-size:1.2rem;font-weight:800;letter-spacing:-.015em}
.st-sub{margin-top:4px!important;font-size:.86rem;color:var(--st-muted)}
.st-box{appearance:none;flex:none;width:20px;height:20px;margin:0;border:2px solid var(--st-box);border-radius:6px;background:#fff center/13px no-repeat;cursor:pointer;
  transition:background-color .15s,border-color .15s}
.st-box:checked{background-color:var(--st-accent);border-color:var(--st-accent);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E")}
.st-box:indeterminate{background-color:var(--st-accent);border-color:var(--st-accent);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E")}
.st-box:focus-visible{outline:3px solid var(--st-accent);outline-offset:2px}
.st-box.sm{width:18px;height:18px;border-radius:5px;background-size:12px}
.st-tree{margin-top:12px!important}
.st-task{border-top:1px solid var(--st-line);padding:6px 0}
.st-row{display:flex;align-items:center;gap:10px;min-height:44px}
.st-tog{flex:none;display:grid;place-items:center;width:30px;height:30px;padding:0;margin-left:-6px;border:0;border-radius:8px;background:transparent;color:var(--st-muted);cursor:pointer;transition:background-color .15s}
.st-tog:hover{background:var(--st-tint);color:var(--st-ink)}
.st-tog svg{transition:transform .2s ease}
.st-tog[aria-expanded="true"] svg{transform:rotate(90deg)}
.st-tog:focus-visible{outline:3px solid var(--st-accent);outline-offset:1px}
.st-title{flex:1;min-width:0;font-size:1rem;font-weight:650;line-height:1.4;cursor:pointer}
.st-box:checked+.st-title{color:var(--st-muted);text-decoration:line-through}
.st-count{flex:none;font-size:.76rem;font-weight:700;padding:3px 9px;border-radius:999px;background:var(--st-tint);color:var(--st-accent-dark);white-space:nowrap}
.st-subs{margin:2px 0 8px 33px!important;padding-left:19px!important;border-left:2px solid var(--st-guide)}
.st-subs li{display:flex;align-items:center;gap:10px;padding:6px 0}
.st-subs label,.st-tsubs label{flex:1;min-width:0;font-size:.95rem;line-height:1.4;cursor:pointer}
.st-box:checked+label{color:var(--st-muted);text-decoration:line-through}
/* two-level checklist with tree lines */
.st-ttree{margin-top:14px!important;display:grid;gap:10px}
.st-ttask{padding:12px 14px 8px;border:1px solid var(--st-line);border-radius:14px;background:#fffafc}
.st-trow{display:flex;align-items:center;gap:10px}
.st-trow label{flex:1;min-width:0;font-size:1rem;font-weight:700;line-height:1.4;cursor:pointer}
.st-tsubs{margin:6px 0 0 9px!important}
.st-tsubs li{position:relative;display:flex;align-items:center;gap:10px;padding:6px 0 6px 26px}
.st-tsubs li::before{content:"";position:absolute;left:0;top:0;width:16px;height:50%;border-left:2px solid var(--st-guide);border-bottom:2px solid var(--st-guide);border-bottom-left-radius:7px}
.st-tsubs li:not(:last-child)::after{content:"";position:absolute;left:0;top:50%;bottom:0;border-left:2px solid var(--st-guide)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:480px){.st{padding:20px 16px 14px}.st-subs{margin-left:30px!important;padding-left:14px!important}.st-ttask{padding:12px 12px 8px}}
@media (prefers-reduced-motion:reduce){.st *{transition:none!important}}
JavaScript
/* Subtasks: a parent checkbox ticks or clears all of its subtasks, and ticking only some of them puts the parent
   in the indeterminate (mixed) state. Disclosure buttons show and hide a task's subtasks with aria-expanded. */
document.querySelectorAll('[data-tree]').forEach((root) => {
  const live = root.querySelector('[data-live]');
  const parts = (task) => ({
    parent: task.querySelector('[data-parent-box]'),
    kids: [...task.querySelectorAll('[data-subtasks] input[type=checkbox]')],
    count: task.querySelector('[data-count]')
  });
  const sync = (task) => {
    const { parent, kids, count } = parts(task);
    const done = kids.filter((k) => k.checked).length;
    parent.checked = done === kids.length;
    parent.indeterminate = done > 0 && done < kids.length;
    if (count) count.textContent = done + ' of ' + kids.length;
    return [done, kids.length];
  };
  root.querySelectorAll('[data-task]').forEach(sync);

  root.addEventListener('change', (e) => {
    const task = e.target.closest('[data-task]');
    if (!task) return;
    const { parent, kids } = parts(task);
    if (e.target === parent) kids.forEach((k) => { k.checked = parent.checked; });
    const [done, total] = sync(task);
    live.textContent = '“' + parent.labels[0].textContent.trim() + '”: ' + done + ' of ' + total + ' subtasks done.';
  });

  root.querySelectorAll('[aria-controls][aria-expanded]').forEach((btn) => {
    const panel = document.getElementById(btn.getAttribute('aria-controls'));
    panel.hidden = btn.getAttribute('aria-expanded') !== 'true';
    btn.addEventListener('click', () => {
      const open = btn.getAttribute('aria-expanded') !== 'true';
      btn.setAttribute('aria-expanded', String(open));
      panel.hidden = !open;
    });
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Collapsible subtasks</span>
<section class="card st" data-tree aria-labelledby="menu-title">
  <h2 id="menu-title">Launch the summer menu</h2>
  <p class="st-sub">Tick a task to finish all of its subtasks at once.</p>
  <ul class="st-tree">
    <li class="st-task" data-task>
      <div class="st-row"><button class="btn st-tog" type="button" aria-expanded="true" aria-controls="menu-1-subs" aria-label="Subtasks of “Photograph the new dishes”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="form-check-input" type="checkbox" id="menu-1" data-parent-box><label class="st-title" for="menu-1">Photograph the new dishes</label><span class="badge rounded-pill st-count" data-count>2 of 3</span></div>
      <ul class="st-subs" id="menu-1-subs" data-subtasks>
        <li><input class="form-check-input sm" type="checkbox" id="menu-1-1" checked><label for="menu-1-1">Book the photographer</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="menu-1-2" checked><label for="menu-1-2">Prep six hero plates</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="menu-1-3"><label for="menu-1-3">Pick the final twelve shots</label></li>
      </ul>
    </li>
    <li class="st-task" data-task>
      <div class="st-row"><button class="btn st-tog" type="button" aria-expanded="true" aria-controls="menu-2-subs" aria-label="Subtasks of “Print the menus”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="form-check-input" type="checkbox" id="menu-2" data-parent-box><label class="st-title" for="menu-2">Print the menus</label><span class="badge rounded-pill st-count" data-count>0 of 3</span></div>
      <ul class="st-subs" id="menu-2-subs" data-subtasks>
        <li><input class="form-check-input sm" type="checkbox" id="menu-2-1"><label for="menu-2-1">Proof the allergen codes</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="menu-2-2"><label for="menu-2-2">Send the files to Pressworks</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="menu-2-3"><label for="menu-2-3">Collect 200 copies</label></li>
      </ul>
    </li>
    <li class="st-task" data-task>
      <div class="st-row"><button class="btn st-tog" type="button" aria-expanded="false" aria-controls="menu-3-subs" aria-label="Subtasks of “Train the floor team”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="form-check-input" type="checkbox" id="menu-3" data-parent-box checked><label class="st-title" for="menu-3">Train the floor team</label><span class="badge rounded-pill st-count" data-count>2 of 2</span></div>
      <ul class="st-subs" id="menu-3-subs" data-subtasks>
        <li><input class="form-check-input sm" type="checkbox" id="menu-3-1" checked><label for="menu-3-1">Tasting session on Monday</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="menu-3-2" checked><label for="menu-3-2">Hand out the wine pairing notes</label></li>
      </ul>
    </li>
  </ul>
  <p class="visually-hidden" role="status" data-live></p>
</section></div>

<div><span class="lbl">Two-level checklist</span>
<section class="card st" data-tree aria-labelledby="rel-title">
  <h2 id="rel-title">Release 4.2</h2>
  <p class="st-sub">Planned for Wednesday 17 June</p>
  <ul class="st-ttree">
    <li class="st-ttask" data-task>
      <div class="st-trow"><input class="form-check-input" type="checkbox" id="rel-1" data-parent-box checked><label for="rel-1">Code freeze</label><span class="badge rounded-pill st-count" data-count>2 of 2</span></div>
      <ul class="st-tsubs" data-subtasks>
        <li><input class="form-check-input sm" type="checkbox" id="rel-1-1" checked><label for="rel-1-1">Merge the last feature branch</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="rel-1-2" checked><label for="rel-1-2">Tag the release candidate</label></li>
      </ul>
    </li>
    <li class="st-ttask" data-task>
      <div class="st-trow"><input class="form-check-input" type="checkbox" id="rel-2" data-parent-box><label for="rel-2">QA sign-off</label><span class="badge rounded-pill st-count" data-count>1 of 3</span></div>
      <ul class="st-tsubs" data-subtasks>
        <li><input class="form-check-input sm" type="checkbox" id="rel-2-1" checked><label for="rel-2-1">Run the regression suite</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="rel-2-2"><label for="rel-2-2">Test payments on staging</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="rel-2-3"><label for="rel-2-3">Check the Android build</label></li>
      </ul>
    </li>
    <li class="st-ttask" data-task>
      <div class="st-trow"><input class="form-check-input" type="checkbox" id="rel-3" data-parent-box><label for="rel-3">Ship it</label><span class="badge rounded-pill st-count" data-count>0 of 2</span></div>
      <ul class="st-tsubs" data-subtasks>
        <li><input class="form-check-input sm" type="checkbox" id="rel-3-1"><label for="rel-3-1">Publish the release notes</label></li>
        <li><input class="form-check-input sm" type="checkbox" id="rel-3-2"><label for="rel-3-2">Post in #announcements</label></li>
      </ul>
    </li>
  </ul>
  <p class="visually-hidden" role="status" data-live></p>
</section></div>
CSS
/* Subtasks on Bootstrap: .card, .form-check-input (with its native :indeterminate style), .btn and .badge. Every task and subtask is a real checkbox with a label. The script ties a parent to its subtasks,
   including the indeterminate state, and the chevron buttons are disclosure buttons with aria-expanded. */
.st{--bs-card-border-color:var(--st-line);--bs-card-border-radius:18px;--st-ink:#2a0f1c;--st-muted:#6b4a58;--st-line:#f0dce4;--st-guide:#e3c3d0;--st-box:#9a7383;--st-accent:#be185d;--st-accent-dark:#9d174d;--st-tint:#fce7ef;
  background:#fff;color:var(--st-ink);border:1px solid var(--st-line);border-radius:18px;padding:24px 24px 18px;box-shadow:0 18px 40px -28px rgba(42,15,28,.4)}
.st [hidden]{display:none!important}
.st :where(h2,p,ul){margin:0;padding:0}
.st label{margin:0}
.st h2{line-height:normal}
.st ul{list-style:none}
.st h2{font-size:1.2rem;font-weight:800;letter-spacing:-.015em}
.st-sub{margin-top:4px!important;font-size:.86rem;color:var(--st-muted)}
.st .form-check-input{flex:none;width:20px;height:20px;margin:0;border:2px solid var(--st-box);border-radius:6px;background-size:13px;cursor:pointer;
  transition:background-color .15s,border-color .15s}
.st .form-check-input:checked{background-color:var(--st-accent);border-color:var(--st-accent)}
.st .form-check-input:indeterminate{background-color:var(--st-accent);border-color:var(--st-accent)}
.st .form-check-input:focus{box-shadow:none}
.st .form-check-input:focus-visible{outline:3px solid var(--st-accent);outline-offset:2px}
.st .form-check-input.sm{width:18px;height:18px;border-radius:5px;background-size:12px}
.st-tree{margin-top:12px!important}
.st-task{border-top:1px solid var(--st-line);padding:6px 0}
.st-row{display:flex;align-items:center;gap:10px;min-height:44px}
.st .st-tog{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-width:0;--bs-btn-color:var(--st-muted);--bs-btn-hover-color:var(--st-ink);--bs-btn-hover-bg:var(--st-tint);--bs-btn-active-color:var(--st-ink);--bs-btn-active-bg:var(--st-tint);flex:none;display:grid;place-items:center;width:30px;height:30px;padding:0;margin-left:-6px;border:0;border-radius:8px;background:transparent;color:var(--st-muted);cursor:pointer;transition:background-color .15s}
.st-tog:hover{background:var(--st-tint);color:var(--st-ink)}
.st-tog svg{transition:transform .2s ease}
.st-tog[aria-expanded="true"] svg{transform:rotate(90deg)}
.st .st-tog:focus-visible{box-shadow:none;outline:3px solid var(--st-accent);outline-offset:1px}
.st-title{flex:1;min-width:0;font-size:1rem;font-weight:650;line-height:1.4;cursor:pointer}
.st .form-check-input:checked+.st-title{color:var(--st-muted);text-decoration:line-through}
.st-count{--bs-badge-font-size:.76rem;--bs-badge-font-weight:700;--bs-badge-padding-x:9px;--bs-badge-padding-y:3px;--bs-badge-color:var(--st-accent-dark);flex:none;background:var(--st-tint);line-height:normal}
.st-subs{margin:2px 0 8px 33px!important;padding-left:19px!important;border-left:2px solid var(--st-guide)}
.st-subs li{display:flex;align-items:center;gap:10px;padding:6px 0}
.st-subs label,.st-tsubs label{flex:1;min-width:0;font-size:.95rem;line-height:1.4;cursor:pointer}
.st .form-check-input:checked+label{color:var(--st-muted);text-decoration:line-through}
/* two-level checklist with tree lines */
.st-ttree{margin-top:14px!important;display:grid;gap:10px}
.st-ttask{padding:12px 14px 8px;border:1px solid var(--st-line);border-radius:14px;background:#fffafc}
.st-trow{display:flex;align-items:center;gap:10px}
.st-trow label{flex:1;min-width:0;font-size:1rem;font-weight:700;line-height:1.4;cursor:pointer}
.st-tsubs{margin:6px 0 0 9px!important}
.st-tsubs li{position:relative;display:flex;align-items:center;gap:10px;padding:6px 0 6px 26px}
.st-tsubs li::before{content:"";position:absolute;left:0;top:0;width:16px;height:50%;border-left:2px solid var(--st-guide);border-bottom:2px solid var(--st-guide);border-bottom-left-radius:7px}
.st-tsubs li:not(:last-child)::after{content:"";position:absolute;left:0;top:50%;bottom:0;border-left:2px solid var(--st-guide)}
@media (max-width:480px){.st{padding:20px 16px 14px}.st-subs{margin-left:30px!important;padding-left:14px!important}.st-ttask{padding:12px 12px 8px}}
@media (prefers-reduced-motion:reduce){.st *{transition:none!important}}
JavaScript
/* Subtasks: a parent checkbox ticks or clears all of its subtasks, and ticking only some of them puts the parent
   in the indeterminate (mixed) state. Disclosure buttons show and hide a task's subtasks with aria-expanded. */
document.querySelectorAll('[data-tree]').forEach((root) => {
  const live = root.querySelector('[data-live]');
  const parts = (task) => ({
    parent: task.querySelector('[data-parent-box]'),
    kids: [...task.querySelectorAll('[data-subtasks] input[type=checkbox]')],
    count: task.querySelector('[data-count]')
  });
  const sync = (task) => {
    const { parent, kids, count } = parts(task);
    const done = kids.filter((k) => k.checked).length;
    parent.checked = done === kids.length;
    parent.indeterminate = done > 0 && done < kids.length;
    if (count) count.textContent = done + ' of ' + kids.length;
    return [done, kids.length];
  };
  root.querySelectorAll('[data-task]').forEach(sync);

  root.addEventListener('change', (e) => {
    const task = e.target.closest('[data-task]');
    if (!task) return;
    const { parent, kids } = parts(task);
    if (e.target === parent) kids.forEach((k) => { k.checked = parent.checked; });
    const [done, total] = sync(task);
    live.textContent = '“' + parent.labels[0].textContent.trim() + '”: ' + done + ' of ' + total + ' subtasks done.';
  });

  root.querySelectorAll('[aria-controls][aria-expanded]').forEach((btn) => {
    const panel = document.getElementById(btn.getAttribute('aria-controls'));
    panel.hidden = btn.getAttribute('aria-expanded') !== 'true';
    btn.addEventListener('click', () => {
      const open = btn.getAttribute('aria-expanded') !== 'true';
      btn.setAttribute('aria-expanded', String(open));
      panel.hidden = !open;
    });
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b4a58] uppercase">Collapsible subtasks</span>
<section class="rounded-[18px] border border-[#f0dce4] bg-white px-6 pt-6 pb-[18px] text-[#2a0f1c] shadow-[0_18px_40px_-28px_rgba(42,15,28,.4)] max-[480px]:px-4 max-[480px]:pt-5 max-[480px]:pb-3.5" data-tree aria-labelledby="menu-title">
  <h2 class="text-[1.2rem] font-extrabold tracking-[-.015em]" id="menu-title">Launch the summer menu</h2>
  <p class="mt-1 text-[.86rem] text-[#6b4a58]">Tick a task to finish all of its subtasks at once.</p>
  <ul class="mt-3">
    <li class="border-t border-[#f0dce4] py-1.5" data-task>
      <div class="flex min-h-11 items-center gap-2.5"><button class="group/tog -ml-1.5 grid size-[30px] flex-none cursor-pointer place-items-center rounded-lg text-[#6b4a58] transition-colors duration-150 hover:bg-[#fce7ef] hover:text-[#2a0f1c] focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-[#be185d] motion-reduce:transition-none [&>svg]:transition-transform [&>svg]:duration-200 aria-expanded:[&>svg]:rotate-90 motion-reduce:[&>svg]:transition-none" type="button" aria-expanded="true" aria-controls="menu-1-subs" aria-label="Subtasks of “Photograph the new dishes”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-5 rounded-md bg-[length:13px]" type="checkbox" id="menu-1" data-parent-box><label class="min-w-0 flex-1 cursor-pointer text-base leading-[1.4] font-[650] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-1">Photograph the new dishes</label><span class="flex-none whitespace-nowrap rounded-full bg-[#fce7ef] px-[9px] py-[3px] text-[.76rem] font-bold text-[#9d174d]" data-count>2 of 3</span></div>
      <ul class="mt-0.5 mb-2 ml-[33px] border-l-2 border-[#e3c3d0] pl-[19px] max-[480px]:ml-[30px] max-[480px]:pl-3.5" id="menu-1-subs" data-subtasks>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-1-1" checked><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-1-1">Book the photographer</label></li>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-1-2" checked><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-1-2">Prep six hero plates</label></li>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-1-3"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-1-3">Pick the final twelve shots</label></li>
      </ul>
    </li>
    <li class="border-t border-[#f0dce4] py-1.5" data-task>
      <div class="flex min-h-11 items-center gap-2.5"><button class="group/tog -ml-1.5 grid size-[30px] flex-none cursor-pointer place-items-center rounded-lg text-[#6b4a58] transition-colors duration-150 hover:bg-[#fce7ef] hover:text-[#2a0f1c] focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-[#be185d] motion-reduce:transition-none [&>svg]:transition-transform [&>svg]:duration-200 aria-expanded:[&>svg]:rotate-90 motion-reduce:[&>svg]:transition-none" type="button" aria-expanded="true" aria-controls="menu-2-subs" aria-label="Subtasks of “Print the menus”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-5 rounded-md bg-[length:13px]" type="checkbox" id="menu-2" data-parent-box><label class="min-w-0 flex-1 cursor-pointer text-base leading-[1.4] font-[650] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-2">Print the menus</label><span class="flex-none whitespace-nowrap rounded-full bg-[#fce7ef] px-[9px] py-[3px] text-[.76rem] font-bold text-[#9d174d]" data-count>0 of 3</span></div>
      <ul class="mt-0.5 mb-2 ml-[33px] border-l-2 border-[#e3c3d0] pl-[19px] max-[480px]:ml-[30px] max-[480px]:pl-3.5" id="menu-2-subs" data-subtasks>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-2-1"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-2-1">Proof the allergen codes</label></li>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-2-2"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-2-2">Send the files to Pressworks</label></li>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-2-3"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-2-3">Collect 200 copies</label></li>
      </ul>
    </li>
    <li class="border-t border-[#f0dce4] py-1.5" data-task>
      <div class="flex min-h-11 items-center gap-2.5"><button class="group/tog -ml-1.5 grid size-[30px] flex-none cursor-pointer place-items-center rounded-lg text-[#6b4a58] transition-colors duration-150 hover:bg-[#fce7ef] hover:text-[#2a0f1c] focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-[#be185d] motion-reduce:transition-none [&>svg]:transition-transform [&>svg]:duration-200 aria-expanded:[&>svg]:rotate-90 motion-reduce:[&>svg]:transition-none" type="button" aria-expanded="false" aria-controls="menu-3-subs" aria-label="Subtasks of “Train the floor team”"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 3.5L10.5 8 6 12.5"/></svg></button><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-5 rounded-md bg-[length:13px]" type="checkbox" id="menu-3" data-parent-box checked><label class="min-w-0 flex-1 cursor-pointer text-base leading-[1.4] font-[650] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-3">Train the floor team</label><span class="flex-none whitespace-nowrap rounded-full bg-[#fce7ef] px-[9px] py-[3px] text-[.76rem] font-bold text-[#9d174d]" data-count>2 of 2</span></div>
      <ul class="mt-0.5 mb-2 ml-[33px] border-l-2 border-[#e3c3d0] pl-[19px] max-[480px]:ml-[30px] max-[480px]:pl-3.5" id="menu-3-subs" data-subtasks>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-3-1" checked><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-3-1">Tasting session on Monday</label></li>
        <li class="flex items-center gap-2.5 py-1.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="menu-3-2" checked><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="menu-3-2">Hand out the wine pairing notes</label></li>
      </ul>
    </li>
  </ul>
  <p class="sr-only" role="status" data-live></p>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b4a58] uppercase">Two-level checklist</span>
<section class="rounded-[18px] border border-[#f0dce4] bg-white px-6 pt-6 pb-[18px] text-[#2a0f1c] shadow-[0_18px_40px_-28px_rgba(42,15,28,.4)] max-[480px]:px-4 max-[480px]:pt-5 max-[480px]:pb-3.5" data-tree aria-labelledby="rel-title">
  <h2 class="text-[1.2rem] font-extrabold tracking-[-.015em]" id="rel-title">Release 4.2</h2>
  <p class="mt-1 text-[.86rem] text-[#6b4a58]">Planned for Wednesday 17 June</p>
  <ul class="mt-3.5 grid gap-2.5">
    <li class="rounded-[14px] border border-[#f0dce4] bg-[#fffafc] px-3.5 pt-3 pb-2 max-[480px]:px-3" data-task>
      <div class="flex items-center gap-2.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-5 rounded-md bg-[length:13px]" type="checkbox" id="rel-1" data-parent-box checked><label class="min-w-0 flex-1 cursor-pointer text-base leading-[1.4] font-bold peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-1">Code freeze</label><span class="flex-none whitespace-nowrap rounded-full bg-[#fce7ef] px-[9px] py-[3px] text-[.76rem] font-bold text-[#9d174d]" data-count>2 of 2</span></div>
      <ul class="mt-1.5 ml-[9px]" data-subtasks>
        <li class="relative flex items-center gap-2.5 py-1.5 pl-[26px] before:absolute before:top-0 before:left-0 before:h-1/2 before:w-4 before:rounded-bl-[7px] before:border-b-2 before:border-l-2 before:border-[#e3c3d0] [&:not(:last-child)]:after:absolute [&:not(:last-child)]:after:top-1/2 [&:not(:last-child)]:after:bottom-0 [&:not(:last-child)]:after:left-0 [&:not(:last-child)]:after:border-l-2 [&:not(:last-child)]:after:border-[#e3c3d0]"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="rel-1-1" checked><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-1-1">Merge the last feature branch</label></li>
        <li class="relative flex items-center gap-2.5 py-1.5 pl-[26px] before:absolute before:top-0 before:left-0 before:h-1/2 before:w-4 before:rounded-bl-[7px] before:border-b-2 before:border-l-2 before:border-[#e3c3d0] [&:not(:last-child)]:after:absolute [&:not(:last-child)]:after:top-1/2 [&:not(:last-child)]:after:bottom-0 [&:not(:last-child)]:after:left-0 [&:not(:last-child)]:after:border-l-2 [&:not(:last-child)]:after:border-[#e3c3d0]"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="rel-1-2" checked><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-1-2">Tag the release candidate</label></li>
      </ul>
    </li>
    <li class="rounded-[14px] border border-[#f0dce4] bg-[#fffafc] px-3.5 pt-3 pb-2 max-[480px]:px-3" data-task>
      <div class="flex items-center gap-2.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-5 rounded-md bg-[length:13px]" type="checkbox" id="rel-2" data-parent-box><label class="min-w-0 flex-1 cursor-pointer text-base leading-[1.4] font-bold peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-2">QA sign-off</label><span class="flex-none whitespace-nowrap rounded-full bg-[#fce7ef] px-[9px] py-[3px] text-[.76rem] font-bold text-[#9d174d]" data-count>1 of 3</span></div>
      <ul class="mt-1.5 ml-[9px]" data-subtasks>
        <li class="relative flex items-center gap-2.5 py-1.5 pl-[26px] before:absolute before:top-0 before:left-0 before:h-1/2 before:w-4 before:rounded-bl-[7px] before:border-b-2 before:border-l-2 before:border-[#e3c3d0] [&:not(:last-child)]:after:absolute [&:not(:last-child)]:after:top-1/2 [&:not(:last-child)]:after:bottom-0 [&:not(:last-child)]:after:left-0 [&:not(:last-child)]:after:border-l-2 [&:not(:last-child)]:after:border-[#e3c3d0]"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="rel-2-1" checked><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-2-1">Run the regression suite</label></li>
        <li class="relative flex items-center gap-2.5 py-1.5 pl-[26px] before:absolute before:top-0 before:left-0 before:h-1/2 before:w-4 before:rounded-bl-[7px] before:border-b-2 before:border-l-2 before:border-[#e3c3d0] [&:not(:last-child)]:after:absolute [&:not(:last-child)]:after:top-1/2 [&:not(:last-child)]:after:bottom-0 [&:not(:last-child)]:after:left-0 [&:not(:last-child)]:after:border-l-2 [&:not(:last-child)]:after:border-[#e3c3d0]"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="rel-2-2"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-2-2">Test payments on staging</label></li>
        <li class="relative flex items-center gap-2.5 py-1.5 pl-[26px] before:absolute before:top-0 before:left-0 before:h-1/2 before:w-4 before:rounded-bl-[7px] before:border-b-2 before:border-l-2 before:border-[#e3c3d0] [&:not(:last-child)]:after:absolute [&:not(:last-child)]:after:top-1/2 [&:not(:last-child)]:after:bottom-0 [&:not(:last-child)]:after:left-0 [&:not(:last-child)]:after:border-l-2 [&:not(:last-child)]:after:border-[#e3c3d0]"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="rel-2-3"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-2-3">Check the Android build</label></li>
      </ul>
    </li>
    <li class="rounded-[14px] border border-[#f0dce4] bg-[#fffafc] px-3.5 pt-3 pb-2 max-[480px]:px-3" data-task>
      <div class="flex items-center gap-2.5"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-5 rounded-md bg-[length:13px]" type="checkbox" id="rel-3" data-parent-box><label class="min-w-0 flex-1 cursor-pointer text-base leading-[1.4] font-bold peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-3">Ship it</label><span class="flex-none whitespace-nowrap rounded-full bg-[#fce7ef] px-[9px] py-[3px] text-[.76rem] font-bold text-[#9d174d]" data-count>0 of 2</span></div>
      <ul class="mt-1.5 ml-[9px]" data-subtasks>
        <li class="relative flex items-center gap-2.5 py-1.5 pl-[26px] before:absolute before:top-0 before:left-0 before:h-1/2 before:w-4 before:rounded-bl-[7px] before:border-b-2 before:border-l-2 before:border-[#e3c3d0] [&:not(:last-child)]:after:absolute [&:not(:last-child)]:after:top-1/2 [&:not(:last-child)]:after:bottom-0 [&:not(:last-child)]:after:left-0 [&:not(:last-child)]:after:border-l-2 [&:not(:last-child)]:after:border-[#e3c3d0]"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="rel-3-1"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-3-1">Publish the release notes</label></li>
        <li class="relative flex items-center gap-2.5 py-1.5 pl-[26px] before:absolute before:top-0 before:left-0 before:h-1/2 before:w-4 before:rounded-bl-[7px] before:border-b-2 before:border-l-2 before:border-[#e3c3d0] [&:not(:last-child)]:after:absolute [&:not(:last-child)]:after:top-1/2 [&:not(:last-child)]:after:bottom-0 [&:not(:last-child)]:after:left-0 [&:not(:last-child)]:after:border-l-2 [&:not(:last-child)]:after:border-[#e3c3d0]"><input class="peer flex-none cursor-pointer appearance-none border-2 border-[#9a7383] bg-white bg-center bg-no-repeat transition-colors duration-150 checked:border-[#be185d] checked:bg-[#be185d] checked:bg-tick indeterminate:border-[#be185d] indeterminate:bg-[#be185d] indeterminate:bg-dash focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#be185d] motion-reduce:transition-none size-[18px] rounded-[5px] bg-[length:12px]" type="checkbox" id="rel-3-2"><label class="min-w-0 flex-1 cursor-pointer text-[.95rem] leading-[1.4] peer-checked:text-[#6b4a58] peer-checked:line-through" for="rel-3-2">Post in #announcements</label></li>
      </ul>
    </li>
  </ul>
  <p class="sr-only" role="status" data-live></p>
</section></div>
CSS
/* the tick and the indeterminate dash drawn inside the boxes */
@utility bg-tick {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E");
}
@utility bg-dash {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.6' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
}
JavaScript
/* Subtasks: a parent checkbox ticks or clears all of its subtasks, and ticking only some of them puts the parent
   in the indeterminate (mixed) state. Disclosure buttons show and hide a task's subtasks with aria-expanded. */
document.querySelectorAll('[data-tree]').forEach((root) => {
  const live = root.querySelector('[data-live]');
  const parts = (task) => ({
    parent: task.querySelector('[data-parent-box]'),
    kids: [...task.querySelectorAll('[data-subtasks] input[type=checkbox]')],
    count: task.querySelector('[data-count]')
  });
  const sync = (task) => {
    const { parent, kids, count } = parts(task);
    const done = kids.filter((k) => k.checked).length;
    parent.checked = done === kids.length;
    parent.indeterminate = done > 0 && done < kids.length;
    if (count) count.textContent = done + ' of ' + kids.length;
    return [done, kids.length];
  };
  root.querySelectorAll('[data-task]').forEach(sync);

  root.addEventListener('change', (e) => {
    const task = e.target.closest('[data-task]');
    if (!task) return;
    const { parent, kids } = parts(task);
    if (e.target === parent) kids.forEach((k) => { k.checked = parent.checked; });
    const [done, total] = sync(task);
    live.textContent = '“' + parent.labels[0].textContent.trim() + '”: ' + done + ' of ' + total + ' subtasks done.';
  });

  root.querySelectorAll('[aria-controls][aria-expanded]').forEach((btn) => {
    const panel = document.getElementById(btn.getAttribute('aria-controls'));
    panel.hidden = btn.getAttribute('aria-expanded') !== 'true';
    btn.addEventListener('click', () => {
      const open = btn.getAttribute('aria-expanded') !== 'true';
      btn.setAttribute('aria-expanded', String(open));
      panel.hidden = !open;
    });
  });
});

About this set

Checklists where a task has subtasks and the parent box reflects them. Collapsible subtasks is a restaurant’s summer menu launch with three tasks, each with a chevron button that shows or hides its subtasks, a count such as 2 of 3, and a guide line down the left of the nested list. Two-level checklist is a software release in three bordered cards whose subtasks hang from tree connector lines and are always visible. In both, ticking a parent ticks or clears all of its subtasks, ticking every subtask ticks the parent, and ticking only some of them puts the parent in the indeterminate state, drawn as a dash. The indeterminate state can only be set from JavaScript, so a script of about thirty lines keeps the parents, the counts and the disclosure buttons in step and announces the new count. Without the script every subtask stays visible and every box still works on its own. The plain version draws the tick and the dash with background images, Bootstrap uses the native .form-check-input:indeterminate style, and Tailwind uses the indeterminate: variant.

What’s included

  • Parent checkbox ticks or clears all of its subtasks
  • Indeterminate parent drawn as a dash when only some subtasks are done
  • Collapsible subtasks with disclosure buttons and a count per task
  • Two-level checklist with tree connector lines
  • Subtasks stay visible and usable without JavaScript

Accessibility

Every task and subtask is a real checkbox with a label, and the parent's indeterminate property is exposed to screen readers as mixed. The chevrons are disclosure buttons with aria-expanded and aria-controls, named after their task, and a polite status region announces counts such as Print the menus: 3 of 3 subtasks done. Controls show a visible focus outline, and the chevron rotation and colour transitions stop under prefers-reduced-motion.

Customise it

Change --st-accent, --st-accent-dark, --st-guide and --st-box on .st for the boxes, counts and guide lines. Bootstrap uses the same variables on .form-check-input and .badge; in Tailwind swap the #be185d and #e3c3d0 colour utilities.