See how different frameworks handle parent-child relationships
Switch to desktop view to compare frameworks side by side
---
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>
<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>
<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>
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>
);
}
<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>
---
// @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>
<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>
---
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>
<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>