Nested Checkboxes

See how different frameworks handle parent-child relationships

Switch to desktop view to compare frameworks side by side

Complexity: 50%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.31kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

Vanilla JavaScript

---
const parentCheckbox = {
	id: "parent-vanilla",
	label: "Parent",
};

const childCheckboxItems = [
	{ id: "child1-vanilla", label: "Child 1" },
	{ id: "child2-vanilla", label: "Child 2" },
	{ id: "child3-vanilla", label: "Child 3" },
];
---

<div class="px-3 py-2" data-checkbox-container>
  <div>
    <div class="p-2 rounded-lg hover:bg-slate-100 transition-colors">
      <div class="flex items-center space-x-3">
        <input 
          type="checkbox" 
          id={parentCheckbox.id}
          class="h-4 w-4 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
        >
        <label for={parentCheckbox.id} class="text-slate-700 cursor-pointer text-sm font-medium">
          {parentCheckbox.label}
        </label>
      </div>
    </div>

    <div class="ml-8">
      {childCheckboxItems.map(item => (
        <div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
          <div class="flex items-center space-x-3">
            <input 
              type="checkbox" 
              id={item.id}
              class="h-4 w-4 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
            >
            <label for={item.id} class="text-slate-700 cursor-pointer text-sm font-medium">
              {item.label}
            </label>
          </div>
        </div>
      ))}
    </div>
  </div>
</div>

<script is:inline>
document.querySelectorAll('[data-checkbox-container]').forEach(container => {
  const parent = container.querySelector('input:first-of-type');
  const children = container.querySelectorAll('.ml-8 input');

  function updateParent() {
    const total = children.length;
    const checked = [...children].filter(c => c.checked).length;
    
    parent.checked = checked === total;
    parent.indeterminate = checked > 0 && checked < total;
  }

  parent.addEventListener('change', () => {
    children.forEach(child => child.checked = parent.checked);
  });

  children.forEach(child => 
    child.addEventListener('change', updateParent)
  );
});
</script> 
Complexity: 25%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.31kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

Alpine.js

<div x-data="{
	parent: { id: 'parent', label: 'Parent' },
	checkboxes: [
		{ id: 'child1', label: 'Child 1', checked: false },
		{ id: 'child2', label: 'Child 2', checked: false },
		{ id: 'child3', label: 'Child 3', checked: false }
	]
}">
	<div class="px-3 py-2">
		<div>
			<div class="p-2 rounded-lg hover:bg-slate-100 transition-colors">
				<div class="flex items-center space-x-3">
					<input 
						type="checkbox" 
						:id="parent.id" 
						class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
						:checked="checkboxes.every(c => c.checked)"
						@change="checkboxes.forEach(c => c.checked = $event.target.checked)"
						x-effect="$el.indeterminate = checkboxes.some(c => c.checked) && !checkboxes.every(c => c.checked)"
					>
					<label 
						:for="parent.id" 
						class="text-slate-700 cursor-pointer text-sm font-medium"
						x-text="parent.label"
					></label>
				</div>
			</div>

			<div class="ml-8">
				<template x-for="checkbox in checkboxes" :key="checkbox.id">
					<div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
						<div class="flex items-center space-x-3">
							<input 
								type="checkbox" 
								:id="checkbox.id"
								class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
								x-model="checkbox.checked"
							>
							<label 
								:for="checkbox.id" 
								class="text-slate-700 cursor-pointer text-sm font-medium"
								x-text="checkbox.label"
							></label>
						</div>
					</div>
				</template>
			</div>
		</div>
	</div>
</div>
Complexity: 55%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.31kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

Vue

<script setup>
import { computed, ref } from "vue";

const checkboxes = ref([
	{ id: "child1-vue", label: "Child 1", checked: false },
	{ id: "child2-vue", label: "Child 2", checked: false },
	{ id: "child3-vue", label: "Child 3", checked: false },
]);

const parent = {
	id: "parent-vue",
	label: "Parent",
};

const allChecked = computed(() => checkboxes.value.every((c) => c.checked));
const someChecked = computed(() => checkboxes.value.some((c) => c.checked));

function toggleParent(e) {
	const newValue = e.target.checked;
	for (const c of checkboxes.value) c.checked = newValue;
}
</script>

<template>
    <div class="px-3 py-2">
        <div>
            <div class="p-2 rounded-lg hover:bg-slate-100 transition-colors">
                <div class="flex items-center space-x-3">
                    <input type="checkbox" :id="parent.id"
                        class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
                        :checked="allChecked" :indeterminate="someChecked && !allChecked" @change="toggleParent" />
                    <label :for="parent.id" class="text-slate-700 cursor-pointer text-sm font-medium">
                        {{ parent.label }}
                    </label>
                </div>
            </div>

            <div class="ml-8">
                <div v-for="checkbox in checkboxes" :key="checkbox.id"
                    class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
                    <div class="flex items-center space-x-3">
                        <input type="checkbox" :id="checkbox.id"
                            class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
                            v-model="checkbox.checked" />
                        <label :for="checkbox.id" class="text-slate-700 cursor-pointer text-sm font-medium">
                            {{ checkbox.label }}
                        </label>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
Complexity: 60%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 1.53kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

React

import { useCallback, useEffect, useRef, useState } from "react";

function Checkbox({
	id,
	label,
	checked,
	indeterminate,
	onChange,
	className = "",
}) {
	const ref = useRef();

	useEffect(() => {
		if (ref.current) {
			ref.current.indeterminate = indeterminate;
		}
	}, [indeterminate]);

	return (
		<div
			className={`rounded-lg hover:bg-slate-100 transition-colors ${className}`}
		>
			<div className="flex items-center space-x-3">
				<input
					ref={ref}
					type="checkbox"
					id={id}
					checked={checked}
					onChange={onChange}
					className="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
				/>
				<label
					htmlFor={id}
					className="text-slate-700 cursor-pointer text-sm font-medium"
				>
					{label}
				</label>
			</div>
		</div>
	);
}

export default function NestedCheckboxes() {
	const [checkboxes, setCheckboxes] = useState([
		{ id: "child1-react", label: "Child 1", checked: false },
		{ id: "child2-react", label: "Child 2", checked: false },
		{ id: "child3-react", label: "Child 3", checked: false },
	]);

	const allChecked = checkboxes.every((item) => item.checked);
	const someChecked = checkboxes.some((item) => item.checked);

	const handleParentChange = useCallback((e) => {
		const newValue = e.target.checked;
		setCheckboxes((boxes) =>
			boxes.map((box) => ({ ...box, checked: newValue })),
		);
	}, []);

	const handleChildChange = useCallback(
		(id) => (e) => {
			setCheckboxes((boxes) =>
				boxes.map((box) =>
					box.id === id ? { ...box, checked: e.target.checked } : box,
				),
			);
		},
		[],
	);

	return (
		<div className="px-3 py-2">
			<Checkbox
				id="parent-react"
				label="Parent"
				checked={allChecked}
				indeterminate={someChecked && !allChecked}
				onChange={handleParentChange}
				className="p-2"
			/>

			<div className="ml-8">
				{checkboxes.map((item) => (
					<Checkbox
						key={item.id}
						{...item}
						onChange={handleChildChange(item.id)}
						className="p-1"
					/>
				))}
			</div>
		</div>
	);
}
Complexity: 35%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.31kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

Svelte

<div class="px-3 py-2">
  <div>
    <div class="p-2 rounded-lg hover:bg-slate-100 transition-colors">
      <div class="flex items-center space-x-3">
        <input
          type="checkbox"
          id="parent-checkbox"
          class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
          checked={allChecked}
          indeterminate={isIndeterminate}
          onchange={toggleAll}
        />
        <label
          for="parent-checkbox"
          class="text-slate-700 cursor-pointer text-sm font-medium"
        >
          Parent
        </label>
      </div>
    </div>
    <div class="ml-8">
      {#each checkboxes as checkbox, i}
        <div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
          <div class="flex items-center space-x-3">
            <input
              type="checkbox"
              id="child-{i}"
              class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
              bind:checked={checkbox.value}
            />
            <label
              for="child-{i}"
              class="text-slate-700 cursor-pointer text-sm font-medium"
            >
              {checkbox.label}
            </label>
          </div>
        </div>
      {/each}
    </div>
  </div>
</div>

<script>
let checkboxes = $state([
	{ label: "Child 1", value: false },
	{ label: "Child 2", value: false },
	{ label: "Child 3", value: false },
]);

const allChecked = $derived(checkboxes.every((v) => v.value));
const someChecked = $derived(checkboxes.some((v) => v.value));
const isIndeterminate = $derived(someChecked && !allChecked);

function toggleAll() {
	const newValue = !allChecked;
	for (const v of checkboxes) v.value = newValue;
}
</script>
Complexity: 20%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.64kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

Hyperscript

---
// @ts-nocheck
const parentCheckbox = {
	id: "parent-hyperscript",
	label: "Parent",
};

const childCheckboxItems = [
	{ id: "child1-hyperscript", label: "Child 1" },
	{ id: "child2-hyperscript", label: "Child 2" },
	{ id: "child3-hyperscript", label: "Child 3" },
];
---
<script src="https://unpkg.com/hyperscript.org@0.9.13"></script>

<div class="px-3 py-2">
  <div>
    <div class="p-2 rounded-lg hover:bg-slate-100 transition-colors">
      <div class="flex items-center space-x-3">
        <input
          type="checkbox"
          id={parentCheckbox.id}
          class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
          _="on click
                set the checked of <input[type=checkbox]/> in the next <div/> to my checked
              on click from the next <div/>
                set my checked to no <input:not(:checked)/> in it
                set my indeterminate to <:checked/> in it exists and <input:not(:checked)/> in it exists"
        />
        <label
          for={parentCheckbox.id}
          class="text-slate-700 cursor-pointer text-sm font-medium"
        >
          {parentCheckbox.label}
        </label>
      </div>
    </div>
    <div class="ml-8">
      {childCheckboxItems.map(item => (
        <div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
          <div class="flex items-center space-x-3">
            <input
              type="checkbox"
              id={item.id}
              class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
            />
            <label
              for={item.id}
              class="text-slate-700 cursor-pointer text-sm font-medium"
            >
              {item.label}
            </label>
          </div>
        </div>
      ))}
    </div>
  </div>
</div> 
Complexity: 10%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.31kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

CSS Only

<div class="px-3 py-2">
  <div class="checkbox-group">
    <!-- Parent checkbox -->
    <div class="p-2 rounded-lg hover:bg-slate-100 transition-colors">
      <div class="flex items-center space-x-3">
        <input type="checkbox" class="parent-checkbox" id="parent-css" />
        <label for="parent-css" class="text-slate-700 cursor-pointer text-sm font-medium">
          Parent
        </label>
      </div>
    </div>

    <!-- Child checkboxes -->
    <div class="ml-8">
      <div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
        <div class="flex items-center space-x-3">
          <input type="checkbox" class="child-checkbox" id="child1-css" />
          <label for="child1-css" class="text-slate-700 cursor-pointer text-sm font-medium">
            Child 1
          </label>
        </div>
      </div>
      <div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
        <div class="flex items-center space-x-3">
          <input type="checkbox" class="child-checkbox" id="child2-css" />
          <label for="child2-css" class="text-slate-700 cursor-pointer text-sm font-medium">
            Child 2
          </label>
        </div>
      </div>
      <div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
        <div class="flex items-center space-x-3">
          <input type="checkbox" class="child-checkbox" id="child3-css" />
          <label for="child3-css" class="text-slate-700 cursor-pointer text-sm font-medium">
            Child 3
          </label>
        </div>
      </div>
    </div>
  </div>
</div>

<style>
/* Base styles */
.checkbox-group input[type="checkbox"] {
  height: 1rem;
  width: 1rem;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  border: 1px solid rgb(126, 137, 147);
  border-radius: 0.20rem;
  vertical-align: -0.125em;
  position: relative;
  margin-top: 0.125rem;
  background-color: transparent;
}

/* Default checked style (for both parent & child) */
.checkbox-group input[type="checkbox"]:checked {
  background-color: rgb(37 99 235);
  border-color: transparent;
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}

/* 
  PARTIAL CHECK / INDETERMINATE VISUAL
  Show a dash if the parent is NOT checked, but some child is checked,
  AND some child is not checked. 
*/
.checkbox-group:has(.parent-checkbox:not(:checked)):has(.child-checkbox:checked):has(.child-checkbox:not(:checked))
  .parent-checkbox {
  background-color: rgb(37 99 235);
  border-color: rgb(37 99 235);
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M3 8h10' stroke='white' stroke-width='2'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: center;
}

/* 
  PARENT LOOKS CHECKED (VISUALLY) IF
  - The parent itself is NOT actually checked, but
  - All child checkboxes are checked 
*/
.checkbox-group:has(.parent-checkbox:not(:checked)):has(.child-checkbox:checked):not(:has(.child-checkbox:not(:checked)))
  .parent-checkbox {
  background-color: rgb(37 99 235);
  border-color: rgb(37 99 235);
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: center;
}

/* When the parent is actually checked, visually override the child states to look checked. 
   (In pure CSS, we can't truly set the child's checked property, but we can force their styling.) 
*/
.checkbox-group:has(.parent-checkbox:checked) .child-checkbox {
  background-color: rgb(37 99 235) !important;
  border-color: rgb(37 99 235) !important;
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e") !important;
  background-repeat: no-repeat !important;
  background-position: center !important;
}

/* Add focus styles */
.checkbox-group input[type="checkbox"]:focus {
  outline: 2px solid transparent;
  outline-offset: 2px;
  box-shadow: 0 0 0 2px rgb(255 255 255), 0 0 0 4px rgb(59 130 246);
}
</style>
  
Complexity: 45%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.63kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

jQuery

---
const parentCheckbox = {
	id: "parent-jquery",
	label: "Parent",
};

const childCheckboxItems = [
	{ id: "child1-jquery", label: "Child 1" },
	{ id: "child2-jquery", label: "Child 2" },
	{ id: "child3-jquery", label: "Child 3" },
];
---

<div class="px-3 py-2" data-checkbox-container>
  <div>
    <div class="p-2 rounded-lg hover:bg-slate-100 transition-colors">
      <div class="flex items-center space-x-3">
        <input 
          type="checkbox" 
          id={parentCheckbox.id}
          class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
        >
        <label for={parentCheckbox.id} class="text-slate-700 cursor-pointer text-sm font-medium">
          {parentCheckbox.label}
        </label>
      </div>
    </div>

    <div class="ml-8">
      {childCheckboxItems.map(item => (
        <div class="p-1 rounded-lg hover:bg-slate-100 transition-colors">
          <div class="flex items-center space-x-3">
            <input 
              type="checkbox" 
              id={item.id}
              class="h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
            >
            <label for={item.id} class="text-slate-700 cursor-pointer text-sm font-medium">
              {item.label}
            </label>
          </div>
        </div>
      ))}
    </div>
  </div>
</div>

<script>
import $ from 'jquery';

$('[data-checkbox-container]').each(function() {
  const $container = $(this);
  const $parent = $container.find('input:first');
  const $children = $container.find('.ml-8 input');

  function updateParent() {
    const total = $children.length;
    const checked = $children.filter(':checked').length;
    
    $parent.prop('checked', checked === total);
    $parent.prop('indeterminate', checked > 0 && checked < total);
  }

  $parent.on('change', () => {
    $children.prop('checked', $parent.prop('checked'));
  });

  $children.on('change', updateParent);
});
</script> 
Complexity: 30%
Complexity score is calculated using Gemini 1.5 Pro based on: • State Management (40%): How state is stored and updated • Event Handling (35%): Parent-child interactions • Code Overhead (25%): Boilerplate and helpers needed
JS Bundle: 0.63kb
JS Bundle size represents: • Total size of all JavaScript files • Measured after compression • Includes framework code • Captured during page load

Stimulus

Parent-child checkbox group
<style>
  .stimulus-container { @apply px-3 py-2 }
  .stimulus-parent { @apply p-2 rounded-lg }
  .stimulus-children { @apply ml-8 rounded-lg }
  .stimulus-checkbox { @apply p-1 flex items-center space-x-3 hover:bg-slate-100 transition-colors }
  .stimulus-checkbox-input { @apply h-4 w-4 mt-0.5 text-blue-600 rounded border-slate-300 focus:ring-blue-500 }
  .stimulus-checkbox-label { @apply text-slate-700 cursor-pointer text-sm font-medium }
</style>

<fieldset data-controller="checkbox-group" class="stimulus-container" aria-label="Checkbox group">
  <legend class="sr-only">Parent-child checkbox group</legend>

  <div class="stimulus-parent">
    <div class="stimulus-checkbox">
      <input id="stimulus-parent" type="checkbox" data-checkbox-group-target="parent" data-action="checkbox-group#parentToggle" class="stimulus-checkbox-input" aria-controls="stimulus-children" />
      <label for="stimulus-parent" class="stimulus-checkbox-label">Parent</label>
    </div>
  </div>

  <div id="stimulus-children" class="stimulus-children">
    { [1, 2, 3].map((i) =>
      <div class="stimulus-checkbox">
        <input id={`stimulus-child-${i}`} type="checkbox" data-checkbox-group-target="child" data-action="checkbox-group#childToggle" class="stimulus-checkbox-input" />
        <label for={`stimulus-child-${i}`} class="stimulus-checkbox-label">Child {i}</label>
      </div> )}
  </div>
</fieldset>

<script>
  import { Application, Controller } from "@hotwired/stimulus"

  window.Stimulus = Application.start()

  window.Stimulus.register("checkbox-group", class extends Controller {
    static targets = [ 'parent', 'child' ]

    declare readonly parentTarget: HTMLInputElement
    declare readonly childTargets: HTMLInputElement[]

    parentToggle() {
      const checked = this.parentTarget.checked

      this.childTargets.forEach(child => child.checked = checked)
    }

    childToggle() {
      const allChecked = this.childTargets.every(child => child.checked)
      const someChecked = this.childTargets.some(child => child.checked)

      this.parentTarget.checked = allChecked || someChecked
      this.parentTarget.indeterminate = someChecked && !allChecked
    }
  })
</script>