lexington®
Use code LEX35 at checkout




7k+ customers.
How to create a grouped checkbox tree with Alpine.js and Tailwind CSS
Learn how to build an interactive grouped checkbox tree component using Alpine.js for state management and Tailwind CSS for styling, perfect for notification preferences.
Published on October 14, 2025 by Michael AndreuzzaGrouped checkbox trees are useful for organizing related options in a hierarchical structure, allowing users to select entire groups or individual items. This tutorial shows how to build a flexible checkbox tree with group-level controls and individual item selection using Alpine.js and Tailwind CSS.
What we’ll build
We’ll create a checkbox tree that:
- Organizes options into groups with sub-items
- Includes a master “All” checkbox to select/deselect everything
- Features group checkboxes that control all items in that group
- Supports individual item selection
- Updates checkbox states (checked, indeterminate) dynamically
- Uses smooth, accessible styling
Prerequisites
Make sure Alpine.js and Tailwind CSS are installed in your project. If you’re using Astro, you can include this component directly in any page or layout.
How it works
The component uses Alpine.js to manage the hierarchical state. The x-data
object contains:
groups
: Array of group objects, each with a key, label, and array of itemsselected
: Array of selected item keysallItems()
: Returns all item keys across all groupsisGroupAll(key)
/isGroupNone(key)
: Check if all/none items in a group are selectedtoggleAll()
: Selects/deselects all itemstoggleGroup(key)
: Selects/deselects all items in a specific groupupdate()
: Updates checkbox states (checked/indeterminate) based on selection
The component uses x-model
for two-way binding on individual checkboxes and x-ref
to access checkbox elements for state updates.
Using the component
Basic setup
Include the component in your HTML with the predefined groups:
<div
class="flex flex-col w-full max-w-lg gap-2"
x-data="{ /* component data */ }"
>
<!-- component structure -->
</div>
Customizing groups
Modify the groups
array to fit your needs:
groups: [
{
key: "category1",
label: "Category 1",
items: [
{ key: "item1", label: "Item 1" },
{ key: "item2", label: "Item 2" },
],
},
// more groups...
];
Accessing selected items
The selected
array contains the keys of all selected items. You can use this for form submission or further processing:
console.log($data.selected); // ['login', 'promos', 'comments']
Customizing the component
You can modify the styling by changing Tailwind classes:
- Change colors: Update
text-blue-600
andborder-zinc-300
to your brand colors - Adjust spacing: Modify
gap-2
,pl-6
, etc. - Change sizes: Update
size-4
for checkbox sizes
For different group structures, simply update the groups
array in the x-data
object.
Full code
Here’s the complete component code:
<div
class="flex flex-col w-full max-w-lg gap-2"
x-data="{
groups: [
{ key: 'security', label: 'Security', items: [
{ key: 'login', label: 'Login alerts' },
{ key: 'password', label: 'Password changes' },
]},
{ key: 'marketing', label: 'Marketing', items: [
{ key: 'promos', label: 'Promotions' },
{ key: 'product', label: 'Product updates' },
]},
{ key: 'community', label: 'Community', items: [
{ key: 'comments', label: 'Comments' },
{ key: 'mentions', label: 'Mentions' },
]},
],
selected: [],
allItems(){ return this.groups.flatMap(g => g.items.map(i => i.key)) },
isGroupAll(key){ const g = this.groups.find(x => x.key===key); return g.items.every(i => this.selected.includes(i.key)) },
isGroupNone(key){ const g = this.groups.find(x => x.key===key); return g.items.every(i => !this.selected.includes(i.key)) },
toggleAll($refs){
if (this.selected.length === this.allItems().length) this.selected = []
else this.selected = [...this.allItems()]
this.update($refs)
},
toggleGroup(key,$refs){
const g = this.groups.find(x => x.key===key)
const keys = g.items.map(i => i.key)
if (this.isGroupAll(key)) this.selected = this.selected.filter(k => !keys.includes(k))
else this.selected = Array.from(new Set([...this.selected, ...keys]))
this.update($refs)
},
update($refs){
const setRef = (ref, all, none) => { if (!ref) return; ref.checked = all; ref.indeterminate = !all && !none }
setRef($refs.topRef, this.selected.length === this.allItems().length, this.selected.length === 0)
setRef($refs.secRef, this.isGroupAll('security'), this.isGroupNone('security'))
setRef($refs.mktRef, this.isGroupAll('marketing'), this.isGroupNone('marketing'))
setRef($refs.comRef, this.isGroupAll('community'), this.isGroupNone('community'))
},
init(){ this.update(this.$refs) }
}"
>
<div class="flex items-start gap-3">
<input
id="tree-all"
name="tree-all"
type="checkbox"
x-ref="topRef"
@change="toggleAll($refs)"
class="text-blue-600 rounded shadow size-4 border-zinc-300 focus:ring-blue-600"
/>
<div class="w-full">
<label class="text-base block text-sm text-zinc-700">
All notifications
</label>
<div class="flex flex-col pl-6 mt-2 gap-2">
<!-- Security group -->
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<input
id="tree-sec"
type="checkbox"
x-ref="secRef"
@change="toggleGroup('security', $refs)"
class="text-blue-600 rounded shadow size-4 border-zinc-300 focus:ring-blue-600"
/>
<label for="tree-sec" class="text-sm text-zinc-700">Security</label>
</div>
<template
x-for="item in groups.find(g => g.key==='security').items"
:key="item.key"
>
<div class="flex items-center pl-6 gap-2">
<input
type="checkbox"
:id="`tree-sec-`+item.key"
:value="item.key"
x-model="selected"
@change="update($refs)"
class="text-blue-600 rounded shadow size-4 border-zinc-300 focus:ring-blue-600"
/>
<label
:for="`tree-sec-`+item.key"
class="text-sm text-zinc-600"
x-text="item.label"
></label>
</div>
</template>
</div>
<!-- Marketing group -->
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<input
id="tree-mkt"
type="checkbox"
x-ref="mktRef"
@change="toggleGroup('marketing', $refs)"
class="text-blue-600 rounded shadow size-4 border-zinc-300 focus:ring-blue-600"
/>
<label for="tree-mkt" class="text-sm text-zinc-700"
>Marketing</label
>
</div>
<template
x-for="item in groups.find(g => g.key==='marketing').items"
:key="item.key"
>
<div class="flex items-center pl-6 gap-2">
<input
type="checkbox"
:id="`tree-mkt-`+item.key"
:value="item.key"
x-model="selected"
@change="update($refs)"
class="text-blue-600 rounded shadow size-4 border-zinc-300 focus:ring-blue-600"
/>
<label
:for="`tree-mkt-`+item.key"
class="text-sm text-zinc-600"
x-text="item.label"
></label>
</div>
</template>
</div>
<!-- Community group -->
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<input
id="tree-com"
type="checkbox"
x-ref="comRef"
@change="toggleGroup('community', $refs)"
class="text-blue-600 rounded shadow size-4 border-zinc-300 focus:ring-blue-600"
/>
<label for="tree-com" class="text-sm text-zinc-700"
>Community</label
>
</div>
<template
x-for="item in groups.find(g => g.key==='community').items"
:key="item.key"
>
<div class="flex items-center pl-6 gap-2">
<input
type="checkbox"
:id="`tree-com-`+item.key"
:value="item.key"
x-model="selected"
@change="update($refs)"
class="text-blue-600 rounded shadow size-4 border-zinc-300 focus:ring-blue-600"
/>
<label
:for="`tree-com-`+item.key"
class="text-sm text-zinc-600"
x-text="item.label"
></label>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
This component provides a user-friendly way to manage hierarchical selections, perfect for notification preferences, feature toggles, or any grouped options interface.
/Michael Andreuzza