@xsolla/xui-multi-select
v0.202.4
Published
A cross-platform multi-select control that lets users pick multiple options from a dropdown list.
Downloads
18,813
Readme
MultiSelect
A cross-platform multi-select control that lets users pick multiple options from a dropdown list.
A dropdown input that allows selecting multiple values simultaneously. Displays the current selections inside the field as text labels or removable tag chips, opens a ContextMenu with checkboxes when clicked, and supports an optional count tag when selections overflow. Built on the same trigger pattern as Select but with multi-value semantics.
When to use
- When the user must choose more than one value from a predefined list — multiple tags, categories, permissions, countries, features
- In filter bars where the user applies several filters simultaneously
- In forms where a many-to-many relationship must be configured (e.g. assigning roles, selecting supported platforms)
- When the full list of options must remain visible and re-selectable without navigating away
When not to use
- When only one value can be selected at a time — use Select
- When the number of selectable options is very small (2–4) and all options should be permanently visible — use Checkbox group or ToggleButtonGroup
- When the user needs to type free-form tags or values not from a predefined list — use a tag input component
- When the user needs to rank or order selections — use a dedicated sortable list
Content guidelines
- Placeholder text — use a concise instructional phrase: "Select categories", "Choose platforms", "Pick permissions". Avoid generic "Select…" alone.
- Option labels — keep each option label short (1–4 words). Use parallel structure across all options (all nouns or all short phrases). No trailing punctuation.
- Option order — alphabetical or most common first. Do not randomise.
- Count tag format — always use +N format (e.g. +3, +12). Do not spell out "3 more" inside a chip.
- Clear button — enable for optional fields. Do not show on required fields — clearing all values on a required field immediately produces a validation error.
- Field label — always provide a visible label above the field via FieldGroup.
- Error messages — be specific:
- "Please select at least one category" (required, empty on submit)
- "You can select a maximum of 5 items" (max exceeded)
Behaviour guidelines
- Multi-select model — multiple values can be selected independently. Selecting one value does not deselect others. Unchecking an option in the ContextMenu removes it from the field.
- Individual removal (Tag variant) — when Variant=Tag and Remove button=true, clicking ✕ on a chip removes that specific value from the selection without reopening the dropdown. The ContextMenu reflects the change if it is open.
- Clear all — clicking the clear button (✕, Clear button=true) removes all selections at once and returns the field to Fill=False.
- Count tag — when selections overflow the visible content area, show a +N count tag. Clicking it opens the dropdown panel so the user can review all selected values.
- Opening — clicking the field or pressing Enter / Space / ↓ when focused opens the ContextMenu. The field enters State=Focus; the chevron rotates upward.
- Closing — the ContextMenu closes on: clicking outside, pressing Escape, or pressing Tab. Focus returns to the MultiSelect field.
- Validation — validate on blur (field loses focus with the dropdown closed) or on form submit. Show State=Error with a message if the selection is invalid (e.g. required field with no selection, or max selection exceeded).
- Disabled — State=Disable prevents opening the dropdown. Existing selections remain visible in muted style.
- ContextMenu width — the ContextMenu panel must be at least as wide as the MultiSelect field. Never open a narrower panel.
- Real-time update — the field content updates as the user checks/unchecks options. Do not batch updates to the field until the dropdown closes.
Accessibility
- MultiSelect must have role="combobox" (or role="button") with aria-haspopup="listbox" and aria-expanded="true" / "false".
- The field must have an accessible name via (from FieldGroup), aria-label, or aria-labelledby.
- The selected values must be reflected in aria-label or as visually visible text so screen readers can announce the current state — e.g. aria-label="Categories: Design, Development, Marketing".
- When Variant=Tag and Remove button=true, each chip's remove button must have aria-label="Remove [value name]" — e.g. aria-label="Remove Design".
- The count tag (+N) must have aria-label describing the hidden count — e.g. aria-label="3 more selected items".
- The clear button must have aria-label="Clear all selections".
- When State=Error, the error message must be linked via aria-describedby and in an aria-live="polite" region.
- The ContextMenu dropdown uses role="listbox" (or role="menu") with each option as role="option" / role="menuitemcheckbox" and aria-checked="true" / "false".
- Keyboard navigation: Enter / Space / ↓ opens the dropdown; ↑ / ↓ navigate options; Space toggles the focused option; Escape closes without further changes; Tab closes and moves focus forward.
- When a value is added or removed (via chip ✕ or from dropdown), announce the change via aria-live="polite": "Design added" or "Design removed".
Installation
npm install @xsolla/xui-multi-selectImports
import { MultiSelect } from "@xsolla/xui-multi-select";
import type {
MultiSelectProps,
MultiSelectOption,
MultiSelectValue,
MultiSelectVariant,
MultiSelectSize,
MultiSelectState,
} from "@xsolla/xui-multi-select";Quick start
const options = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
];
const [selected, setSelected] = useState<MultiSelectValue>([]);
<MultiSelect
label="Frameworks"
options={options}
value={selected}
onChange={setSelected}
placeholder="Select frameworks"
/>;External panel (B2B grouped select)
When the option list is rendered elsewhere (for example @xsolla/xui-b2b-group-select), set dropdownMenu={false} so the control does not open the built-in list. Wire the same value / onChange to both components; use onTriggerPress to toggle your panel, menuOpen for chevron/open styling, and menuMinWidth (default 540, aligned with GROUP_SELECT_MIN_PANEL_WIDTH) so the field matches the panel width.
import * as React from "react";
import { MultiSelect } from "@xsolla/xui-multi-select";
import {
GroupSelect,
GROUP_SELECT_MIN_PANEL_WIDTH,
type GroupSelectGroup,
} from "@xsolla/xui-b2b-group-select";
const groups: GroupSelectGroup[] = [
/* ... */
];
const flatOptions = groups.flatMap((g) =>
g.items.map((it) => ({ value: it.id, label: it.label }))
);
export default function GroupedFieldShell() {
const [value, setValue] = React.useState<string[]>([]);
const [open, setOpen] = React.useState(false);
return (
<>
<MultiSelect
options={flatOptions}
value={value}
onChange={(v) => setValue(v.map(String))}
placeholder="Select regions"
size="sm"
dropdownMenu={false}
menuOpen={open}
menuMinWidth={GROUP_SELECT_MIN_PANEL_WIDTH}
onTriggerPress={() => setOpen((o) => !o)}
/>
{open && (
<GroupSelect groups={groups} value={value} onChange={setValue} />
)}
</>
);
}Add backdrop, click-outside, and Escape handling in your layout as needed (see Storybook).
API Reference
<MultiSelect>
| Prop | Type | Default | Description |
| ------------------- | --------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| testID | string | — | Test ID for testing frameworks. On web this renders as data-testid; on React Native it renders as testID. |
| options | MultiSelectOption[] | — | Available options. |
| value | MultiSelectValue | [] | Selected values. |
| onChange | (values: MultiSelectValue) => void | — | Fired when the selection changes. |
| placeholder | string | 'Select' | Placeholder shown when empty. |
| label | string | — | Label rendered above the control. |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Control size. |
| state | 'default' \| 'hover' \| 'focus' \| 'disable' \| 'error' | — | Forced visual state. |
| disabled | boolean | false | Disable the control. |
| errorMessage | string | — | Error message; also marks the control invalid. |
| variant | 'tag' \| 'text' | 'tag' | How selected options are displayed. |
| flexible | boolean | true | When true the control grows with content; otherwise fixed-height. |
| removeTagsButtons | boolean | true | Show a remove button on each tag. |
| extraClear | boolean | false | Show a clear-all button. |
| maxHeight | number | 300 | Maximum dropdown height in pixels. |
| searchable | boolean | false | Show a search input at the top of the built-in dropdown that filters options in memory by label. Web only. |
| searchPlaceholder | string | 'Search' | Placeholder for the search input (used when searchable). |
| noOptionsMessage | string | 'No results' | Message shown when the search filter matches no options. |
| iconLeft | ReactNode | — | Icon rendered on the left of the control. |
| iconRight | ReactNode | — | Icon on the right (overrides the default caret). |
| dropdownMenu | boolean | true | When false, hides the built-in list and disables click-to-open; use with an external picker (e.g. GroupSelect) wired to the same value / onChange. |
| onTriggerPress | () => void | — | When dropdownMenu is false: fired when the user activates the field. Typically toggles the external panel. |
| menuOpen | boolean | false | When dropdownMenu is false: drives chevron direction and layering like the built-in open state. |
| menuMinWidth | number | 540 | When dropdownMenu is false: field min-width in px (matches GroupSelect). Use 0 for no minimum. |
Inherits ThemeOverrideProps (themeMode, themeProductContext).
Types
type MultiSelectValue = (string | number)[];
type MultiSelectVariant = "tag" | "text";
type MultiSelectSize = "xs" | "sm" | "md" | "lg" | "xl";
type MultiSelectState = "default" | "hover" | "focus" | "disable" | "error";
interface MultiSelectOption {
label: ReactNode;
value: string | number;
disabled?: boolean;
}Examples
Sizes
const options = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
];
<MultiSelect options={options} size="xs" placeholder="Extra small" />
<MultiSelect options={options} size="sm" placeholder="Small" />
<MultiSelect options={options} size="md" placeholder="Medium" />
<MultiSelect options={options} size="lg" placeholder="Large" />
<MultiSelect options={options} size="xl" placeholder="Extra large" />Text variant with clear-all
const options = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
];
const [selected, setSelected] = useState<MultiSelectValue>([]);
<MultiSelect
options={options}
value={selected}
onChange={setSelected}
variant="text"
extraClear
/>;Error state
const options = [
{ label: "Design", value: "design" },
{ label: "Engineering", value: "engineering" },
{ label: "Product", value: "product" },
];
const [skills, setSkills] = useState<MultiSelectValue>([]);
<MultiSelect
label="Skills"
options={options}
value={skills}
onChange={setSkills}
errorMessage="Please select at least one skill"
/>;Disabled
const options = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
];
<MultiSelect options={options} value={["react"]} disabled />;Searchable
Enable searchable to show a search field at the top of the dropdown. It filters the options in memory by label as the user types, mirroring the Select component's searchable behavior. Customize the input hint with searchPlaceholder and the empty-result text with noOptionsMessage. Web only — the prop is ignored on native.
const options = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
{ label: "Solid", value: "solid" },
];
const [selected, setSelected] = useState<MultiSelectValue>([]);
<MultiSelect
label="Frameworks"
options={options}
value={selected}
onChange={setSelected}
searchable
searchPlaceholder="Search frameworks"
noOptionsMessage="No frameworks found"
placeholder="Select frameworks"
/>;Long option labels
Long, unbreakable option labels (URLs, IDs, tokens) wrap inside the dropdown menu instead of forcing a horizontal scrollbar; the menu width stays pinned to the control.
const options = [
{
label:
"https://prototype.xsolla.dev/mini-apps/some-really-long-slug?token=abcdef0123456789",
value: "url",
},
{ label: "urn:xsolla:resource:0123456789abcdef0123456789abcdef", value: "id" },
{ label: "Short", value: "short" },
];
const [selected, setSelected] = useState<MultiSelectValue>([]);
<MultiSelect
options={options}
value={selected}
onChange={setSelected}
placeholder="Select options"
/>;Accessibility
- Selection is rendered as a checkbox list inside the dropdown.
- The dropdown is keyboard navigable; selection state is announced to assistive technology.
- An
errorMessagemarks the control as invalid for screen readers. - When
searchable, the dropdown's search input exposes anaria-labelmatchingsearchPlaceholder; options filter in memory as the user types (web only).
