Skip to content

Multi Select

SelectionSince 0.9.0
import { MultiSelect } from '@absoluteui/components/multi-select';
import '@absoluteui/components/multi-select/style.css';

Using Angular? See Multi Select for Angular.

An editable combobox with a multi-selectable listbox (WAI-ARIA 1.2) for choosing several values.

  • Selected values appear as chips with remove buttons; typing filters (or loads) options.
  • Activating an option toggles it, and the list stays open for further choices unless closeOnSelect is set.
  • DOM focus stays on the input; the active option is conveyed through aria-activedescendant.
  • The listbox renders in the browser top layer (Popover API) and is positioned by the positioning engine.
  • Choosing several values from a list long enough to benefit from filtering (roughly 7+ options).
  • Tags, skills, recipients, or teams from a known (or searchable) set.
  • A few options that fit on screen: use Checkbox Group.
  • One value: use Combobox or Select.
  • Free-text tags that are not in a list (custom values are not supported).
import { MultiSelect, type MultiSelectChangeDetail } from '@absoluteui/components/multi-select';
const form = document.createElement('form');
const host = document.createElement('div');
form.append(host);
document.body.append(form);
const skills = MultiSelect.create(host, {
label: 'Skills',
name: 'skills',
required: true,
selectAll: true,
maxSelected: 5,
options: [
{ value: 'ts', label: 'TypeScript' },
{ value: 'sql', label: 'SQL' },
{ value: 'ux', label: 'UX research', description: 'Interviews and testing' },
],
value: ['ts'],
});
skills.host.addEventListener('aui-change', (event) => {
const { value, added, removed } = (event as CustomEvent<MultiSelectChangeDetail>).detail;
console.log(value, added, removed);
});
import { MultiSelect, type MultiSelectOption } from '@absoluteui/components/multi-select';
const host = document.createElement('div');
document.body.append(host);
MultiSelect.create(host, {
label: 'Reviewers',
options: [],
loadOptions: async (query, { signal }): Promise<MultiSelectOption[]> => {
const response = await fetch(`/api/people?q=${encodeURIComponent(query)}`, { signal });
const people = (await response.json()) as { id: string; name: string }[];
return people.map((p) => ({ value: p.id, label: p.name }));
},
});
import { MultiSelect, type MultiSelectChangeDetail } from '@absoluteui/components/multi-select';
import '@absoluteui/components/multi-select/style.css';
const host = document.querySelector<HTMLElement>('#tags');
if (host) {
MultiSelect.create(host, { label: 'Tags', options: [{ value: 'urgent', label: 'Urgent' }, { value: 'billing', label: 'Billing' }] });
host.addEventListener('aui-change', (e) => {
console.log((e as CustomEvent<MultiSelectChangeDetail>).detail.value);
});
}
import { MultiSelect } from '@absoluteui/components/multi-select';
const host = document.createElement('div');
document.body.append(host);
MultiSelect.create(host, {
label: 'Skills',
name: 'skills',
selectAll: true,
options: [
{ value: 'ts', label: 'TypeScript' },
{ value: 'sql', label: 'SQL' },
],
});
<div class="aui-multi-select"> (consumer host)
<label class="aui-multi-select__label" for=input>
<div class="aui-multi-select__control">
<ul class="aui-multi-select__chips" aria-label="Selected: {label}">
<li class="aui-multi-select__chip"><span class="aui-multi-select__chip-label"> <button class="aui-multi-select__remove">
</ul>
<input role="combobox" class="aui-multi-select__input">
<button class="aui-multi-select__toggle" tabindex="-1" aria-label="Show options">
</div>
<div class="aui-multi-select__description"> <div class="aui-multi-select__error">
<input type="hidden" name> ... (one per selected value)
<div class="aui-multi-select__popup" popover="manual">
<div role="listbox" aria-multiselectable="true" class="aui-multi-select__listbox">
<div role="option" class="aui-multi-select__option aui-multi-select__option--all">Select all</div>
<div role="option" class="aui-multi-select__option"><span class="aui-multi-select__check"> ... </div>
</div>
<div class="aui-multi-select__empty">No results</div>
</div>
</div>
MultiSelect.create(host: HTMLElement, options: MultiSelectOptions): MultiSelect

Global configuration key: multiSelect (see Configuration).

Option Type Default Description
label string Visible label and accessible name. Required.
options readonly MultiSelectOption[] { value, label, disabled?, description? } with unique values. Required.
value readonly string[] [] Selected values in selection order; unknown and duplicate values are dropped.
name string Form field name: one hidden input per selected value.
placeholder string Input placeholder while nothing is selected (never replaces the label).
description string Help text referenced by aria-describedby.
required boolean false At least one value; value-missing after touch or reportValidity().
disabled boolean false Disables the input, toggle, and chip remove buttons.
readOnly boolean false Shows the value; the list does not open and chips have no remove buttons.
invalid boolean false Consumer-managed invalid state (shown immediately).
errorMessage string Message shown when invalid is true (text only).
maxSelected number Most values that can be selected (integer >= 1); unselected options become unavailable once reached, and reaching it is announced.
selectAll boolean false A “Select all” option that toggles every enabled option shown (up to maxSelected).
filter 'contains' | 'startsWith' | 'none' "contains" Diacritic- and case-insensitive filtering of options while typing. Globally configurable.
closeOnSelect boolean false Close the list after each toggle. Globally configurable.
openOnFocus boolean false Open the list when the input gains focus or is clicked. Globally configurable.
locale string "en-US" Messages and filtering locale. Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" Popup alignment. Globally configurable. Inherited from common configuration.
renderOption (option: MultiSelectOption, context: MultiSelectRenderContext) => Node Custom option content; the label stays the accessible name. Nodes are released with releaseNode when dropped.
loadOptions (query: string, context: MultiSelectLoadContext) => Promise<readonly MultiSelectOption[]> Load options for the typed text instead of filtering options; debounced, aborted by a newer query or closing; loading and errors are shown and announced.
loadDelay number 250 Milliseconds between the last keystroke and loadOptions (0 to 5000).
icons Partial<{ expand: IconRef; selected: IconRef; remove: IconRef; error: IconRef }> Icon slot overrides.
Property Type Description
host HTMLElement Consumer-provided host (children are owned by the component).
input HTMLInputElement The combobox input.
listbox HTMLDivElement The multi-selectable listbox.
value string[] Selected values in selection order (a copy).
selectedOptions MultiSelectOption[] Options of the selected values.
isOpen boolean Whether the listbox is shown.
name string | null Form field name.
label string Accessible label.
disabled boolean Disabled state.
validity FieldValidity { valid, error, message }.
dirty boolean The value differs from the initial value.
touched boolean Focus has left the control.
Method Description
setValue(value: readonly string[]): void Programmatic selection (no aui-change); unknown values are dropped.
update(options: Partial<MultiSelectOptions>): void Merge and apply options.
open(): void Show the listbox.
close(): void Hide the listbox.
focus(): void Focus the input.
getFormValue(): string[] The selected values.
reportValidity(): boolean Show messages now and return validity.
setCustomError(message: string | null): void External error; null clears it.
reset(): void Restore the initial value and clear touched/submitted state (no aui-change).
destroy(): void Remove the component, listeners, and released nodes.
Event Detail Bubbles Cancelable Description
aui-change { value: string[]; added: string[]; removed: string[] } Yes No The user changed the selection (option, Select all, chip remove, or Backspace).
aui-input { query: string } Yes No The user edited the input text.
aui-open null No No The listbox opened.
aui-close null No No The listbox closed.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-multi-select Host.
aui-multi-select__control The input-like boundary around chips, input, and toggle.
aui-multi-select__chip | __chip-label | __remove Selected value chip, its label, and its remove button.
aui-multi-select__input | __toggle Combobox input and list toggle.
aui-multi-select__popup | __listbox | __option | __option--active | __option--all | __check Top-layer popup, listbox, options, active option, Select all, and selection mark.
aui-multi-select--invalid | --disabled State modifiers on the host.
Slot Default icon
expand chevron-down
selected check
remove close
error alert-circle

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-selected | --aui-color-selected-indicator | --aui-color-selected-text | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-control-height-sm | --aui-control-padding-x | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-popover-max-height | --aui-radius-md | --aui-radius-sm | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-touch-target-min

interface MultiSelectOption { readonly value: string; readonly label: string; readonly disabled?: boolean; readonly description?: string }
interface MultiSelectChangeDetail { value: string[]; added: string[]; removed: string[] }
type MultiSelectFilter = 'contains' | 'startsWith' | 'none';

Pattern: WAI-ARIA APG Combobox pattern (editable, list autocomplete, no automatic selection) with a multi-selectable listbox; selected values as a list of chips with remove buttons.

Key Context Action
ArrowDown / ArrowUp Closed Opens and activates the first / last enabled option.
Alt+ArrowDown Closed Opens without activating an option.
ArrowDown / ArrowUp Open Moves the active option, skipping unavailable options, wrapping.
PageDown / PageUp, Ctrl+Home / Ctrl+End Open Moves the active option by 10 / to the first or last.
Enter Open with an active option Toggles it (Select all toggles the options shown); the list stays open unless closeOnSelect.
Backspace Empty input Removes the last selected value.
Escape Open / closed with text Closes the listbox / clears the text.
Tab Any Closes the listbox and moves focus (chip remove buttons are tab stops before the input).
Printable characters Any Edits the text, opens, and filters (or loads).
Element Role
input combobox
listbox container listbox (aria-multiselectable=true)
each option, including Select all option
selected values list of listitems; each remove control is a native button
toggle button button (native, tabindex=-1)
Attribute Element Value
aria-expanded input and toggle true when the listbox is shown
aria-controls input and toggle listbox id
aria-activedescendant input id of the active option; removed when none
aria-multiselectable listbox true
aria-selected option true for selected options; for Select all, when every enabled option shown is selected
aria-disabled option true for disabled options and, once maxSelected is reached, unselected ones
aria-label chip list and remove buttons “Selected: {label}” and “Remove {label}” (localized)
aria-invalid / aria-required / aria-describedby input Reflect invalid, required, description, and error text

DOM focus stays on the input while choosing; options are never focused. Chip remove buttons take focus with Tab; after removing a value focus returns to the input.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.7 Focus Visible AA
2.5.8 Target Size (Minimum) AA
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • Selecting and removing values is announced (“{label} selected”, “{label} removed”) through the shared polite live region, as are result counts, loading errors, and reaching the maximum.
  • Selection is shown with a check mark and aria-selected, never by color alone.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver, TalkBack) is pending.

closed, open, filtering, loading, load error, empty results, option active, option selected, option disabled, maximum reached, disabled, read-only, invalid, required.

From Trigger To
closed ArrowDown/ArrowUp, typing, toggle click open
open Enter on an active option / option click option toggled (aui-change); stays open unless closeOnSelect
open Escape, Tab, outside pointer, blur closed (the typed text is cleared on blur)
any (empty input) Backspace last value removed (aui-change)
selection below maximum value reaches maxSelected unselected options unavailable; announced
  • No automatic selection: typing never changes the value until the user toggles an option.
  • Selections and removals (“{label} selected”, “{label} removed”), result counts, loading errors, and reaching the maximum are announced in the shared polite live region.
  • Selection is shown with a check mark and aria-selected, never by color alone; the active option is outlined (WCAG 1.4.1).
  • Remove buttons meet the minimum target size (touch-target-min, WCAG 2.5.8). After removing a value, focus returns to the input.
  • Verified with axe (WCAG 2.2 AA) with chips and the listbox open in 3 themes; the create budget is checked in tests/e2e/perf-forms.spec.ts.
  • Manual screen-reader verification is pending.

The control fills its container and wraps chips onto new lines. The listbox matches the control width, flips above when space below is insufficient, and caps its height to min(popover-max-height, available space).

Logical properties throughout. Popup alignment follows the direction option or configuration.

Token-driven. In forced-colors mode, selected options use Highlight/HighlightText, the active outline uses Highlight, and chips and the popup use CanvasText boundaries.

configureAbsoluteUI({ components: { multiSelect: { filter: 'startsWith', closeOnSelect: true } } });

Instance options override global and theme defaults. All configurable options are dynamic.

icons.expand (default chevron-down), icons.selected (default check), icons.remove (default close), and icons.error (default alert-circle). Required fields show the shared visual-only asterisk.

Option and chip labels use textContent; loaded options are validated as untrusted data. Positioning writes styles through the CSSOM only, so no style attributes are used. CSP-safe.

No persistence or telemetry. With loadOptions, the consumer performs requests. With name, one hidden input per selected value carries the selection for form submission.

Filtering is O(n) per keystroke using a normalized comparison: NFD form, diacritics removed, locale lowercase. All filtered options are rendered.

Budget in headless Chromium: create with 1,000 options under 100 ms.

Tested with 1,000 options. Larger lists should use loadOptions; virtualization is not available.

  • Unknown and duplicate values in value or setValue() are dropped.
  • Selected values stay selected, with their last known labels, when options or loaded results change; this keeps chips stable with loadOptions. To drop values, call setValue() (no aui-change).
  • Disabled options, and unselected options once maxSelected is reached, cannot be activated with the keyboard or clicked.
  • Select all with a maximum selects options in list order until the maximum is reached.
  • Inside a modal dialog, the listbox renders above the dialog, and Escape closes the listbox before the dialog.
Code Category Cause
MULTI_SELECT_OPTIONS configuration Missing label, non-array options, malformed option, duplicate value, or an invalid option (for example maxSelected below 1). Messages name the property, never the value.
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION configuration Invalid or unknown configuration.
  • Strict selection only; no custom values. Typed text is cleared when focus leaves.
  • No virtualization or option groups.
  • Chips cannot be reordered.
  • Manual screen-reader verification is pending.
  • Non-string option values.
  • Duplicate option values.
  • Free-text tags.