See how different frameworks handle parent-child relationships
Switch to desktop view to compare frameworks side by side
Click and drag framework cards to re-order them
---
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="checkbox-demo" data-checkbox-container>
<div>
<input type="checkbox" id={parentCheckbox.id}>
<label for={parentCheckbox.id}>{parentCheckbox.label}</label>
</div>
<div class="checkbox-children">
{childCheckboxItems.map(item => (
<div>
<input type="checkbox" id={item.id}>
<label for={item.id}>{item.label}</label>
</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('.checkbox-children 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> <script>
import Alpine from "alpinejs";
const globalWindow = window as Window & {
Alpine?: typeof Alpine;
__checkboxesAlpineStarted?: boolean;
};
globalWindow.Alpine = Alpine;
function startAlpine() {
if (globalWindow.__checkboxesAlpineStarted) return;
globalWindow.__checkboxesAlpineStarted = true;
Alpine.start();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startAlpine, { once: true });
} else {
startAlpine();
}
</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="checkbox-demo">
<div>
<input
type="checkbox"
:id="parent.id"
: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" x-text="parent.label"></label>
</div>
<div class="checkbox-children">
<template x-for="checkbox in checkboxes" :key="checkbox.id">
<div>
<input
type="checkbox"
:id="checkbox.id"
x-model="checkbox.checked"
>
<label :for="checkbox.id" x-text="checkbox.label"></label>
</div>
</template>
</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="checkbox-demo">
<div>
<input type="checkbox" :id="parent.id" :checked="allChecked"
:indeterminate="someChecked && !allChecked" @change="toggleParent" />
<label :for="parent.id">{{ parent.label }}</label>
</div>
<div class="checkbox-children">
<div v-for="checkbox in checkboxes" :key="checkbox.id">
<input type="checkbox" :id="checkbox.id" v-model="checkbox.checked" />
<label :for="checkbox.id">{{ checkbox.label }}</label>
</div>
</div>
</div>
</template> import { useCallback, useEffect, useRef, useState } from "react";
function Checkbox({ id, label, checked, indeterminate, onChange }) {
const ref = useRef();
useEffect(() => {
if (ref.current) {
ref.current.indeterminate = indeterminate;
}
}, [indeterminate]);
return (
<div>
<input
ref={ref}
type="checkbox"
id={id}
checked={checked}
onChange={onChange}
/>
<label htmlFor={id}>{label}</label>
</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="checkbox-demo">
<Checkbox
id="parent-react"
label="Parent"
checked={allChecked}
indeterminate={someChecked && !allChecked}
onChange={handleParentChange}
/>
<div className="checkbox-children">
{checkboxes.map((item) => (
<Checkbox
key={item.id}
{...item}
onChange={handleChildChange(item.id)}
/>
))}
</div>
</div>
);
} <div class="checkbox-demo">
<div>
<input
type="checkbox"
id="parent-checkbox"
checked={allChecked}
indeterminate={isIndeterminate}
onchange={toggleAll}
/>
<label for="parent-checkbox">Parent</label>
</div>
<div class="checkbox-children">
{#each checkboxes as checkbox, i}
<div>
<input
type="checkbox"
id="child-{i}"
bind:checked={checkbox.value}
/>
<label for="child-{i}">{checkbox.label}</label>
</div>
{/each}
</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="checkbox-demo">
<div>
<input
type="checkbox"
id={parentCheckbox.id}
_="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}>{parentCheckbox.label}</label>
</div>
<div class="checkbox-children">
{childCheckboxItems.map(item => (
<div>
<input type="checkbox" id={item.id} />
<label for={item.id}>{item.label}</label>
</div>
))}
</div>
</div> <div class="checkbox-demo checkbox-group">
<!-- Parent checkbox -->
<div>
<input type="checkbox" class="parent-checkbox" id="parent-css" />
<label for="parent-css">Parent</label>
</div>
<!-- Child checkboxes -->
<div class="checkbox-children">
<div>
<input type="checkbox" class="child-checkbox" id="child1-css" />
<label for="child1-css">Child 1</label>
</div>
<div>
<input type="checkbox" class="child-checkbox" id="child2-css" />
<label for="child2-css">Child 2</label>
</div>
<div>
<input type="checkbox" class="child-checkbox" id="child3-css" />
<label for="child3-css">Child 3</label>
</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="checkbox-demo" data-checkbox-container>
<div>
<input type="checkbox" id={parentCheckbox.id}>
<label for={parentCheckbox.id}>{parentCheckbox.label}</label>
</div>
<div class="checkbox-children">
{childCheckboxItems.map(item => (
<div>
<input type="checkbox" id={item.id}>
<label for={item.id}>{item.label}</label>
</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('.checkbox-children 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> Contributed by Eduardo Bohrer
<fieldset data-controller="checkbox-group" class="checkbox-demo" aria-label="Checkbox group">
<legend class="sr-only">Parent-child checkbox group</legend>
<div>
<input id="stimulus-parent" type="checkbox" data-checkbox-group-target="parent" data-action="checkbox-group#parentToggle" aria-controls="stimulus-children" />
<label for="stimulus-parent">Parent</label>
</div>
<div id="stimulus-children" class="checkbox-children">
{ [1, 2, 3].map((i) =>
<div>
<input id={`stimulus-child-${i}`} type="checkbox" data-checkbox-group-target="child" data-action="checkbox-group#childToggle" />
<label for={`stimulus-child-${i}`}>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> Contributed by Karthikeyan B
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.1/bundles/datastar.js"></script>
<div
class="checkbox-demo"
data-signals="{ child1: false, child2: false, child3: false }"
data-computed:checked="($child1?1:0)+($child2?1:0)+($child3?1:0)"
data-computed:parent="$checked === 3"
data-computed:indeterminate="$checked > 0 && $checked < 3"
>
<div>
<input
type="checkbox"
id="parent-datastar"
data-ref:parent-input
data-bind:parent
data-on:change="$child1=$child2=$child3=event.target.checked"
data-effect="$parentInput.indeterminate = $indeterminate"
/>
<label for="parent-datastar">Parent</label>
</div>
<div class="checkbox-children">
<div>
<input type="checkbox" id="child1-datastar" data-bind:child1 />
<label for="child1-datastar">Child 1</label>
</div>
<div>
<input type="checkbox" id="child2-datastar" data-bind:child2 />
<label for="child2-datastar">Child 2</label>
</div>
<div>
<input type="checkbox" id="child3-datastar" data-bind:child3 />
<label for="child3-datastar">Child 3</label>
</div>
</div>
</div>