multi-select-builder
Build multi-select dropdowns with checkboxes, search, select all. Headless UI Listbox, React Select. Keywords - multi select, multiple select, select with checkboxes, searchable select, headless ui listbox, react select multi
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: multi-select-builder
description: Build multi-select dropdowns with checkboxes, search, select all. Headless UI Listbox, React Select. Keywords - multi select, multiple select, select with checkboxes, searchable select, headless ui listbox, react select multi
---
# Multi-Select Builder
## When to Use This Skill
Activate when:
- Need multi-select dropdowns (select multiple options)
- Building tag/category selectors
- Creating filters with multiple selections
- Want searchable multi-select
- Need "Select All" functionality
## What This Skill Does
Creates multi-select components with:
- Multiple selection with checkboxes
- Search/filter options
- Select all / Deselect all
- Selected items as chips/tags
- Keyboard navigation
- Accessibility
## Supported Technologies
**React**:
- Headless UI Listbox (customizable)
- React Select (feature-rich)
- Downshift (headless)
**Vue 3**:
- Headless UI Vue Listbox
- Vue Multiselect
## Example: Multi-Select with Headless UI (React)
```tsx
// components/MultiSelect.tsx
import { Fragment } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, ChevronUpDownIcon, XMarkIcon } from '@heroicons/react/20/solid';
interface Option {
id: number;
name: string;
}
interface MultiSelectProps {
options: Option[];
selected: Option[];
onChange: (selected: Option[]) => void;
placeholder?: string;
}
export function MultiSelect({
options,
selected,
onChange,
placeholder = 'Select options',
}: MultiSelectProps) {
const handleRemove = (id: number) => {
onChange(selected.filter((item) => item.id !== id));
};
return (
<Listbox value={selected} onChange={onChange} multiple>
<div className="relative">
{/* Selected items as chips */}
{selected.length > 0 && (
<div className="mb-2 flex flex-wrap gap-2">
{selected.map((item) => (
<span
key={item.id}
className="inline-flex items-center gap-1 rounded-full bg-indigo-100 px-3 py-1 text-sm text-indigo-800"
>
{item.name}
<button
onClick={(e) => {
e.stopPropagation();
handleRemove(item.id);
}}
className="hover:text-indigo-600"
type="button"
>
<XMarkIcon className="h-4 w-4" />
</button>
</span>
))}
</div>
)}
{/* Dropdown button */}
<Listbox.Button className="relative w-full cursor-pointer rounded-lg border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<span className="block truncate">
{selected.length === 0
? placeholder
: `${selected.length} selected`}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" />
</span>
</Listbox.Button>
{/* Options dropdown */}
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{options.map((option) => (
<Listbox.Option
key={option.id}
value={option}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
active ? 'bg-indigo-100' : 'text-gray-900'
}`
}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
{option.name}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-indigo-600">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
);
}
// Usage
function TagSelector() {
const [selectedTags, setSelectedTags] = useState<Option[]>([]);
const tags: Option[] = [
{ id: 1, name: 'JavaScript' },
{ id: 2, name: 'TypeScript' },
{ id: 3, name: 'React' },
{ id: 4, name: 'Vue' },
];
return (
<MultiSelect
options={tags}
selected={selectedTags}
onChange={setSelectedTags}
placeholder="Select tags"
/>
);
}
```
## Example: Searchable Multi-Select
```tsx
// components/SearchableMultiSelect.tsx
import { useState, useMemo } from 'react';
import { Combobox } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/20/solid';
interface SearchableMultiSelectProps {
options: Option[];
selected: Option[];
onChange: (selected: Option[]) => void;
placeholder?: string;
}
export function SearchableMultiSelect({
options,
selected,
onChange,
placeholder = 'Search and select',
}: SearchableMultiSelectProps) {
const [query, setQuery] = useState('');
const filteredOptions = useMemo(() => {
if (query === '') return options;
return options.filter((option) =>
option.name.toLowerCase().includes(query.toLowerCase())
);
}, [options, query]);
const handleSelect = (newSelected: Option[]) => {
onChange(newSelected);
setQuery(''); // Clear search after selection
};
const handleRemove = (id: number) => {
onChange(selected.filter((item) => item.id !== id));
};
return (
<Combobox value={selected} onChange={handleSelect} multiple>
<div className="relative">
{/* Selected chips */}
{selected.length > 0 && (
<div className="mb-2 flex flex-wrap gap-2">
{selected.map((item) => (
<span
key={item.id}
className="inline-flex items-center gap-1 rounded-full bg-indigo-100 px-3 py-1 text-sm text-indigo-800"
>
{item.name}
<button
onClick={() => handleRemove(item.id)}
className="hover:text-indigo-600"
type="button"
>
<XMarkIcon className="h-4 w-4" />
</button>
</span>
))}
</div>
)}
{/* Search input */}
<Combobox.Input
className="w-full rounded-lg border border-gray-300 py-2 pl-3 pr-10 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
onChange={(event) => setQuery(event.target.value)}
value={query}
placeholder={placeholder}
/>
{/* Options dropdown */}
<Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5">
{filteredOptions.length === 0 && query !== '' ? (
<div className="relative cursor-default select-none py-2 px-4 text-gray-700">
No results found.
</div>
) : (
filteredOptions.map((option) => (
<Combobox.Option
key={option.id}
value={option}
disabled={selected.some((item) => item.id === option.id)}
className={({ active, disabled }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
disabled
? 'text-gray-400 cursor-not-allowed'
: active
? 'bg-indigo-600 text-white'
: 'text-gray-900'
}`
}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
{option.name}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-indigo-600">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</div>
</Combobox>
);
}
```
## Example: Multi-Select with Select All
```tsx
// components/MultiSelectWithSelectAll.tsx
import { Listbox } from '@headlessui/react';
export function MultiSelectWithSelectAll({
options,
selected,
onChange,
}: MultiSelectProps) {
const handleSelectAll = () => {
onChange(options);
};
const handleDeselectAll = () => {
onChange([]);
};
const allSelected = selected.length === options.length;
const someSelected = selected.length > 0 && selected.length < options.length;
return (
<Listbox value={selected} onChange={onChange} multiple>
<div className="relative">
<Listbox.Button className="relative w-full cursor-pointer rounded-lg border border-gray-300 bg-white py-2 pl-3 pr-10 text-left">
<span className="block truncate">
{selected.length === 0
? 'Select options'
: allSelected
? 'All selected'
: `${selected.length} selected`}
</span>
</Listbox.Button>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5">
{/* Select All / Deselect All */}
<div className="border-b border-gray-200 pb-2">
<button
onClick={allSelected ? handleDeselectAll : handleSelectAll}
className="w-full px-4 py-2 text-left text-sm font-medium text-indigo-600 hover:bg-indigo-50"
type="button"
>
{allSelected ? 'Deselect All' : 'Select All'}
</button>
</div>
{/* Options */}
{options.map((option) => (
<Listbox.Option
key={option.id}
value={option}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
active ? 'bg-indigo-100' : ''
}`
}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
{option.name}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-indigo-600">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
);
}
```
## Example: Vue 3 Multi-Select
```vue
<!-- components/MultiSelect.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from '@headlessui/vue';
interface Option {
id: number;
name: string;
}
const props = defineProps<{
options: Option[];
modelValue: Option[];
}>();
const emit = defineEmits<{
'update:modelValue': [selected: Option[]];
}>();
const handleRemove = (id: number) => {
const newSelected = props.modelValue.filter((item) => item.id !== id);
emit('update:modelValue', newSelected);
};
</script>
<template>
<Listbox
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
multiple
>
<div class="relative">
<!-- Selected chips -->
<div v-if="modelValue.length > 0" class="mb-2 flex flex-wrap gap-2">
<span
v-for="item in modelValue"
:key="item.id"
class="inline-flex items-center gap-1 rounded-full bg-indigo-100 px-3 py-1 text-sm text-indigo-800"
>
{{ item.name }}
<button
@click.stop="handleRemove(item.id)"
class="hover:text-indigo-600"
type="button"
>
×
</button>
</span>
</div>
<!-- Dropdown button -->
<ListboxButton
class="relative w-full cursor-pointer rounded-lg border border-gray-300 bg-white py-2 pl-3 pr-10 text-left"
>
<span class="block truncate">
{{ modelValue.length === 0 ? 'Select options' : `${modelValue.length} selected` }}
</span>
</ListboxButton>
<!-- Options -->
<ListboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5"
>
<ListboxOption
v-for="option in options"
:key="option.id"
:value="option"
v-slot="{ active, selected }"
class="relative cursor-pointer select-none py-2 pl-10 pr-4"
:class="active ? 'bg-indigo-100' : ''"
>
<span :class="selected ? 'font-medium' : 'font-normal'">
{{ option.name }}
</span>
<span
v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3 text-indigo-600"
>
✓
</span>
</ListboxOption>
</ListboxOptions>
</div>
</Listbox>
</template>
```
## Best Practices
1. **Show selected count** - Display "X selected" in button
2. **Remove chips** - Allow removing individual selections
3. **Search functionality** - Filter options in large lists
4. **Select all** - Provide "Select All" / "Deselect All"
5. **Keyboard navigation** - Arrow keys, Enter, Escape
6. **Max selections** - Limit number of selections if needed
7. **Accessibility** - ARIA attributes, screen reader support
## Integration with Other Skills
- **form-builder-with-validation** - Multi-select in forms
- **search-filter-builder** - Multi-select filters
- **api-integration-layer** - Send selected IDs to API
---
**Version**: 1.0.0
**Category**: Frontend Extended
**Complexity**: Medium