Multi Select
Select multiple values with tag-style chips, search, and keyboard navigation.
Two implementation paths are shown side-by-side throughout. Tom Select handles search, keyboard navigation, and ARIA with a single attribute — use it when the option list is long or the UX must be polished. The Alpine.js variant covers simple checkbox-list multi-select where a full library would be overkill. Choose the lightest tool that satisfies the requirement.
1 — Checkbox List
Pure DaisyUI + Alpine. No extra library. Best when the option count is small and all choices should be visible at once without interaction.
Default
selected
With Select All
Disabled
<div
x-data="{
options: [
{ value: 'electronics', label: 'Electronics' },
{ value: 'clothing', label: 'Clothing' },
// …
],
selected: [],
toggle(v) {
this.selected.includes(v)
? this.selected = this.selected.filter(s => s !== v)
: this.selected.push(v);
}
}"
>
<fieldset class="fieldset bg-base-100 border border-base-300 rounded-[--radius-sm] p-3 gap-0">
<legend class="fieldset-legend text-xs font-medium px-1">Categories</legend>
<template x-for="opt in options" :key="opt.value">
<label class="fieldset-label cursor-pointer py-1">
<input
type="checkbox"
class="checkbox checkbox-sm checkbox-primary"
:checked="selected.includes(opt.value)"
@change="toggle(opt.value)"
/>
<span class="text-sm" x-text="opt.label"></span>
</label>
</template>
</fieldset>
</div>
2 — Tag Pills with Dropdown
Selected values render as removable pill badges above a dropdown panel.
Alpine handles open/close; @click.outside closes the panel
on any outside interaction without custom JS.
Default
Max 3 selections
/ selected
<div
x-data="{
open: false,
options: [ { value: 'react', label: 'React' }, /* … */ ],
selected: [],
toggle(v) {
this.selected.includes(v)
? this.selected = this.selected.filter(s => s !== v)
: this.selected.push(v);
},
label(v) { return this.options.find(o => o.value === v)?.label ?? v; }
}"
@click.outside="open = false"
class="relative"
>
<!-- Trigger -->
<div class="min-h-10 w-full flex flex-wrap gap-1.5 items-center px-3 py-2
border border-base-300 rounded-[--radius-sm] bg-base-100 cursor-pointer"
@click="open = !open">
<template x-for="v in selected" :key="v">
<span class="badge badge-soft badge-primary badge-sm gap-1 pr-1">
<span x-text="label(v)"></span>
<button type="button" @click.stop="toggle(v)">✕</button>
</span>
</template>
</div>
<!-- Panel -->
<div x-show="open" x-collapse
class="absolute z-20 left-0 right-0 mt-1 bg-base-100 border
border-base-300 rounded-[--radius-sm] shadow-md overflow-hidden">
<ul class="py-1 max-h-52 overflow-y-auto">
<template x-for="opt in options" :key="opt.value">
<li>
<label class="flex items-center gap-2.5 px-3 py-1.5 text-sm
cursor-pointer hover:bg-base-200">
<input type="checkbox" class="checkbox checkbox-xs checkbox-primary"
:checked="selected.includes(opt.value)"
@change="toggle(opt.value)" />
<span x-text="opt.label"></span>
</label>
</li>
</template>
</ul>
</div>
</div>
3 — Searchable (Tom Select)
Tom Select wired to a plain <select multiple>.
Built-in search, ARIA, and keyboard navigation come for free.
Use plugins: ['remove_button'] to add per-chip remove
buttons without extra markup.
Searchable — basic
Grouped options
<!-- HTML -->
<select id="my-select" name="items[]" multiple placeholder="Search and select…">
<option value="react" selected>React</option>
<option value="vue">Vue</option>
<!-- … -->
</select>
<!-- JS (after Tom Select CSS + JS are loaded) -->
<script>
new TomSelect('#my-select', {
plugins: ['remove_button'],
maxOptions: 20,
placeholder: 'Search and select…',
});
</script>
<!-- Opt-in flag in PHP front-matter -->
<?php
$useTomSelect = true;
?>
4 — States: Empty & Error
Required field validation and empty-selection states for use in controlled form submissions.
Empty
Error
Please select at least one category.
Loading