Skip to content

Multi Select

Edit on GitHub

Lets users select multiple options from a dropdown list, with optional grouping.

@volvo-cars/css v3.1.0@volvo-cars/react-forms v3.1.0

MultiSelect lets users choose multiple values from a predefined list. For single selection, use Select.

EX30
EX90
XC40
XC60
XC90
Show codeHide code
import { MultiSelect, Option } from '@volvo-cars/react-forms';
import { useState } from 'react';
export function MultiSelectBasic() {
const [selected, setSelected] = useState<string[]>([]);
return (
<MultiSelect
name="models"
label="Car models"
value={selected}
onValueChange={setSelected}
>
<Option value="ex30">EX30</Option>
<Option value="ex90">EX90</Option>
<Option value="xc40">XC40</Option>
<Option value="xc60">XC60</Option>
<Option value="xc90">XC90</Option>
</MultiSelect>
);
}

Use variant="floating-label" to place the label inside the field, floating up when at least one option is selected. See Choosing a variant for when to use it.

EX30
EX90
XC40
XC60
Show codeHide code
import { MultiSelect, Option } from '@volvo-cars/react-forms';
import { useState } from 'react';
export function MultiSelectFloatingLabel() {
const [selected, setSelected] = useState<string[]>([]);
return (
<MultiSelect
name="models"
label="Car models"
variant="floating-label"
value={selected}
onValueChange={setSelected}
>
<Option value="ex30">EX30</Option>
<Option value="ex90">EX90</Option>
<Option value="xc40">XC40</Option>
<Option value="xc60">XC60</Option>
</MultiSelect>
);
}

Pass value and onValueChange to control the selection. onValueChange receives the complete array after each option is toggled.

Pass defaultValue for an uncontrolled field. Native form reset restores the default selection.

EX30
EX90
XC40
XC60
Show codeHide code
import { MultiSelect, Option } from '@volvo-cars/react-forms';
export function MultiSelectUncontrolled() {
return (
<form className="stack-16">
<MultiSelect
name="models"
label="Car models"
defaultValue={['ex30', 'xc60']}
required
>
<Option value="ex30">EX30</Option>
<Option value="ex90">EX90</Option>
<Option value="xc40">XC40</Option>
<Option value="xc60">XC60</Option>
</MultiSelect>
<button type="reset" className="button-outlined">
Reset
</button>
</form>
);
}

MultiSelect uses a visually hidden native multiple select for form serialization and constraint validation. name associates every selected value with the form, and FormData.getAll(name) returns the values in selection order. Use form to associate the field with a form elsewhere in the document.

Disabled fields are omitted from form data. required prevents submission until at least one option is selected, marks the visible trigger invalid, and moves focus to it when validation fails.

Pass defaultOpen to open the listbox after mount. Pass open to open or close it from your own state. Users can still close the listbox, so update open from onOpenChange. onOpenChange does not fire for changes caused by open or defaultOpen. A disabled field stays closed.

Use Option.description for supporting text below an option title. Options are at least 48px high and grow to fit additional content. Pass label when children contain custom markup so keyboard type-ahead and the selected value have a stable text value. Use OptionGroup to group related options under a visible heading.

Render Option directly inside MultiSelect, OptionGroup, or a fragment. The selected label then renders on the server and never changes after hydration. Options rendered by your own components still work, but their selected label appears after the component mounts.

The closed field shows the first selected option and how many more are selected, for example “EX30 (+2)”. This format works in every language and needs no extra translation.

To show different text, pass a function to selectedLabel. It receives the selected values and returns the text to show. The field shows your text as it is, so translate it in your app.

EX30
EX90
XC40
XC60
XC90
Show codeHide code
import { MultiSelect, Option } from '@volvo-cars/react-forms';
import { useState } from 'react';
export function MultiSelectSelectedLabel() {
const [selected, setSelected] = useState<string[]>(['ex30', 'xc40', 'xc90']);
return (
<MultiSelect
name="models"
label="Car models"
value={selected}
onValueChange={setSelected}
// Translate this text in your app.
selectedLabel={(values) => `${values.length} selected`}
>
<Option value="ex30">EX30</Option>
<Option value="ex90">EX90</Option>
<Option value="xc40">XC40</Option>
<Option value="xc60">XC60</Option>
<Option value="xc90">XC90</Option>
</MultiSelect>
);
}

On small screens, the listbox opens as a full-width bottom sheet over a scrim. Long lists scroll inside the sheet while page scrolling is locked. On larger screens, the listbox stays anchored 4px below the field.

volvocars.com
Show codeHide code
import { MultiSelect, Option } from '@volvo-cars/react-forms';
export function MultiSelectResponsive() {
return (
<div className="px-pagemargin mt-32" style={{ minHeight: 520 }}>
<MultiSelect name="models" label="Car model" defaultOpen>
<Option value="ex40">EX40</Option>
<Option value="ex90">EX90</Option>
<Option value="xc40">XC40</Option>
<Option value="xc60">XC60</Option>
<Option value="xc90">XC90</Option>
</MultiSelect>
</div>
);
}

Pass errorMessage to display an error and mark the field invalid. The error is hidden when the field is disabled. Use aria-invalid to force the invalid state without a message.

When nothing is selected, a top-label field shows “Select” in the page language. Typing to jump to an option also uses the page language.

Translate the text you pass in: label, option labels, hint, errorMessage and the text returned by selectedLabel.

See Localization for how the page language is chosen, including on the server.

MultiSelect follows the ARIA combobox pattern with a multiselectable listbox. Key responsibilities from the MultiSelect accessibility contract:

  • Label: Provide a concise, visible label and unique option values.
  • Option markup: Pass label to Option when its children contain markup. Don’t place interactive controls inside an option.
  • Error association: errorMessage marks the trigger invalid and associates the message.
  • Dismissible chips: When you show selections as chips, give each dismiss button a label that names the option, for example “Remove EX90”.
KeyBehavior
Arrow Down, Arrow Up, Enter, SpaceOpen the listbox
Arrow Down / Arrow UpMove to the next or previous enabled option
Enter / SpaceToggle the active option
Printable charactersMove to the matching option; repeat to cycle
EscapeClose the listbox
TabClose the listbox and continue focus navigation
PropTypeRequiredDefault
variantFormFieldVariant-top-label
namestring✓-
The name of the input to use when submitting the form.
labelstring✓-
A concise label for the input.
errorMessagestring--
Set the error message of the select and mark it invalid.
aria-invalidboolean-false
Force the input to be invalid.
valuestring[]--
The currently selected values. Makes the input controlled.
defaultValuestring[]--
The initially selected values of an uncontrolled input.
selectedLabelMultiSelectLabelFormatter--
Override the label shown when one or more options are selected. Receives the array of selected values. Use this when the default "Label (+N)" format does not fit (e.g. locale-specific pluralisation).
onValueChange((values: string[]) => void)--
Fires with the complete selection when an option is toggled.
defaultOpenboolean-false
Opens the listbox after mount. Users can then close and reopen it.
openboolean--
Opens or closes the listbox when the value changes. Users can still close it, so update `open` from `onOpenChange`.
onOpenChange((open: boolean) => void)--
Fires when the listbox opens or closes, except when `open` or `defaultOpen` caused it.
onClickMouseEventHandler<HTMLButtonElement>--
onInvalidFormEventHandler<HTMLSelectElement>--
Fires if the input fails validation on form submit.
hiddenboolean--
idstring--
titlestring--
dirstring--
langstring--
slotstring--
translate"yes" | "no"--
classNamestring--
styleCSSProperties--
tabIndexnumber--
onPointerDownPointerEventHandler<Element>--
onPointerEnterPointerEventHandler<Element>--
onPointerLeavePointerEventHandler<Element>--
onPointerMovePointerEventHandler<Element>--
onPointerUpPointerEventHandler<Element>--
disabledboolean-false
Disables the input. Use sparingly as it can be non-obvious to users why an input has been disabled. Prefer showing validation messages and hints instead.
requiredboolean-false
Makes the input required.
onFocusFocusEventHandler<HTMLButtonElement>--
onBlurFocusEventHandler<HTMLButtonElement>--
aria-describedbystring--
aria-labelledbystring--
formstring--
Id of a form element that this input should be associated with. Defaults to the containing form element.
autoFocusboolean-false
Gives the input focus on page load. Use sparingly as it can be confusing to screen-reader and mobile users.
hintReactNode--
Additional hint or description. Supports text, links and block content such as paragraphs or Markdown output.
onKeyDownKeyboardEventHandler<HTMLButtonElement>--
onKeyUpKeyboardEventHandler<HTMLButtonElement>--
PropTypeRequiredDefault
valuestring✓-
The value submitted to the form and included in `onValueChange` when this option is selected.
labelstring--
Plain-text label used for keyboard type-ahead. Defaults to `children` when it is a string. Set this explicitly when `children` contains non-text content (icons, badges) so type-ahead matches against clean text.
descriptionReactNode--
Supplementary text shown below the option title.
disabledboolean-false
When `true` the option is inert — it remains visible and announced by screen readers but cannot be selected or reached via keyboard navigation.
data-activeboolean--
@internal used by the listbox to reflect keyboard-active state
PropTypeRequiredDefault
labelstring✓-
Visible heading rendered above the group's options.
childrenReactNode✓-
`Option` elements belonging to this group.