Multi Select
Lets users select multiple options from a dropdown list, with optional grouping.
MultiSelect lets users choose multiple values from a predefined list. For single selection, use Select.
Show 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> );}Floating label
Section titled “Floating label”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.
Show 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> );}State and form behavior
Section titled “State and form behavior”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.
Show 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.
Options
Section titled “Options”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.
Selected value
Section titled “Selected value”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.
Show 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> );}Responsive behavior
Section titled “Responsive behavior”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.
Show 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> );}Validation
Section titled “Validation”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.
Localization
Section titled “Localization”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.
Accessibility
Section titled “Accessibility”MultiSelect follows the ARIA combobox pattern with a multiselectable listbox. Key responsibilities from the MultiSelect accessibility contract:
- Label: Provide a concise, visible
labeland unique option values. - Option markup: Pass
labeltoOptionwhen its children contain markup. Don’t place interactive controls inside an option. - Error association:
errorMessagemarks 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”.
Keyboard interactions
Section titled “Keyboard interactions”| Key | Behavior |
|---|---|
Arrow Down, Arrow Up, Enter, Space | Open the listbox |
Arrow Down / Arrow Up | Move to the next or previous enabled option |
Enter / Space | Toggle the active option |
| Printable characters | Move to the matching option; repeat to cycle |
Escape | Close the listbox |
Tab | Close the listbox and continue focus navigation |
MultiSelect
Section titled “MultiSelect”| Prop | Type | Required | Default |
|---|---|---|---|
variant | FormFieldVariant | - | top-label |
name | string | ✓ | - |
| The name of the input to use when submitting the form. | |||
label | string | ✓ | - |
| A concise label for the input. | |||
errorMessage | string | - | - |
| Set the error message of the select and mark it invalid. | |||
aria-invalid | boolean | - | false |
| Force the input to be invalid. | |||
value | string[] | - | - |
| The currently selected values. Makes the input controlled. | |||
defaultValue | string[] | - | - |
| The initially selected values of an uncontrolled input. | |||
selectedLabel | MultiSelectLabelFormatter | - | - |
| 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. | |||
defaultOpen | boolean | - | false |
| Opens the listbox after mount. Users can then close and reopen it. | |||
open | boolean | - | - |
| 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. | |||
onClick | MouseEventHandler<HTMLButtonElement> | - | - |
onInvalid | FormEventHandler<HTMLSelectElement> | - | - |
| Fires if the input fails validation on form submit. | |||
hidden | boolean | - | - |
id | string | - | - |
title | string | - | - |
dir | string | - | - |
lang | string | - | - |
slot | string | - | - |
translate | "yes" | "no" | - | - |
className | string | - | - |
style | CSSProperties | - | - |
tabIndex | number | - | - |
onPointerDown | PointerEventHandler<Element> | - | - |
onPointerEnter | PointerEventHandler<Element> | - | - |
onPointerLeave | PointerEventHandler<Element> | - | - |
onPointerMove | PointerEventHandler<Element> | - | - |
onPointerUp | PointerEventHandler<Element> | - | - |
disabled | boolean | - | 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. | |||
required | boolean | - | false |
| Makes the input required. | |||
onFocus | FocusEventHandler<HTMLButtonElement> | - | - |
onBlur | FocusEventHandler<HTMLButtonElement> | - | - |
aria-describedby | string | - | - |
aria-labelledby | string | - | - |
form | string | - | - |
| Id of a form element that this input should be associated with. Defaults to the containing form element. | |||
autoFocus | boolean | - | false |
| Gives the input focus on page load. Use sparingly as it can be confusing to screen-reader and mobile users. | |||
hint | ReactNode | - | - |
| Additional hint or description. Supports text, links and block content such as paragraphs or Markdown output. | |||
onKeyDown | KeyboardEventHandler<HTMLButtonElement> | - | - |
onKeyUp | KeyboardEventHandler<HTMLButtonElement> | - | - |
Option
Section titled “Option”| Prop | Type | Required | Default |
|---|---|---|---|
value | string | ✓ | - |
| The value submitted to the form and included in `onValueChange` when this option is selected. | |||
label | string | - | - |
| 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. | |||
description | ReactNode | - | - |
| Supplementary text shown below the option title. | |||
disabled | boolean | - | 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-active | boolean | - | - |
| @internal used by the listbox to reflect keyboard-active state | |||
OptionGroup
Section titled “OptionGroup”| Prop | Type | Required | Default |
|---|---|---|---|
label | string | ✓ | - |
| Visible heading rendered above the group's options. | |||
children | ReactNode | ✓ | - |
| `Option` elements belonging to this group. | |||