/// Inputs
MultiSelect
Combobox that holds many values as dismissible Tags, with a checkable, type-to-filter listbox.
import { MultiSelect } from "@protocore/pds";Basics
Pass options with value / label and an optional hint. Selected values render as Tags inside the sunken field; picking an option toggles it, and typing filters the list.
2 scope(s) granted
Max & clearable
Cap the selection with max — unselected options disable once the limit is reached. Add clearable for a one-click reset. Drive value from state and read onValueChange.
1/3 regions selected
Sizes
size matches the control scale used by Input and Select: sm (32) · md (36, default) · lg (40) — it sets the minimum row height as Tags wrap.
When to use it
Reach for MultiSelect when a user picks several values from a known set — tags on a resource, scopes on a token, regions for a deploy. It differs from TagsInput, which lets users type *free-form* values that aren't in a list. Use a Combobox when only one value may be selected, and a Select for a short single-choice list. The listbox follows the ARIA multiselect combobox pattern: focus stays in the input while aria-activedescendant tracks the highlighted option and each option carries aria-selected.
Usage
Do
- Give the input an accessible name via `aria-label` or a wrapping `Field`.
- Use `max` when the field has a hard cap — options gate automatically.
- Use it over a long list of checkboxes when the option set is long.
- Make it `clearable` when 'select none' is a valid state.
Don't
- Don't use it for free-form entry — use TagsInput.
- Don't use it for a single choice — use Combobox or Select.
- Don't pack long descriptions into `label`; each row is one line.
- Don't leave it unlabelled; it announces as a nameless combobox.
Accessibility
| Keys | Action |
|---|---|
| Type | Filter the list; opens the popup and highlights the first match. |
| Arrow Down / Up | Move the active option (opens the popup if closed). |
| Home / End | Jump to the first / last option while open. |
| Enter | Toggle the active option; the popup stays open. |
| Backspace | With an empty query, remove the last selected value. |
| Esc | Close the popup. |
- Implements the ARIA 1.2 multiselect combobox: the `role="combobox"` input keeps focus while `aria-activedescendant` points at the highlighted `role="option"`.
- The listbox is `aria-multiselectable="true"`; selected options expose `aria-selected="true"` and gated options `aria-disabled`.
- Additions, removals, and clears are announced through a polite live region.
MultiSelect props
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | Accessible label for the input (required if no visible label wraps it). |
className | string | — | Merged after the pds class on the root field. |
clearable | boolean | false | Show a clear button once anything is selected. |
defaultValue | string[] | — | Initial selected values in uncontrolled mode. |
disabled | boolean | false | Disable the control. |
filter | ((options: MultiSelectOption[], query: string) => MultiSelectOption[]) | (options: MultiSelectOption[], query: string): MultiSelectOption[] => {
const q = query.trim().toLowerCase();
if (!q) return options;
return options.filter((o) => o.label.toLowerCase().includes(q));
} | Custom filter predicate; defaults to case-insensitive label substring match. |
id | string | — | Root id — also seeds the listbox/option ids. |
invalid | boolean | false | Mark the field invalid — danger border plus `aria-invalid`. |
max | number | — | Maximum number of selections. Options beyond it are disabled. |
onValueChange | ((value: string[]) => void) | — | Fires with the next list whenever a value is toggled or removed. |
options * | MultiSelectOption[] | — | The full option set. |
placeholder | string | Select… | Placeholder for the entry (shown only while nothing is selected). |
size | enum | md | Control height: `sm` (32) · `md` (36, default) · `lg` (40) — the minimum row height. |
value | string[] | — | Controlled selected values. |