MultiSelect
Many picks, shown as chips.
About
Multi-choice dropdown: picks show as removable chips in the trigger, and the panel lists one checkbox row per option under a select-all row. Form-associated: posts one entry per picked value under its name. For a list that stays open use sui-listbox; for a few options that fit on the page use sui-checkbox-group.
Usage
Basic
Picks show as removable chips in the trigger, a role="combobox" with aria-haspopup="listbox" that points at the active row through aria-activedescendant. Each chip's X is a button labeled "Remove {label}", so Tab reaches it and Enter or Space removes the pick.
Open with select-all and filter
The select-all header is the first row of the listbox, so the trigger can address it. It draws an indeterminate box while only some options are picked, and picking never closes the panel.
Grouped options
A group is { label, items } in the options array and draws an eyebrow header over its rows. Select all still picks every enabled option, whichever group it sits in.
Icons and avatars
An option's icon sits between the box and the label, and the chip carries it once picked. render draws the row yourself; a chip stays a chip with the icon and the label, and the label still names the row and drives the filter.
Option descriptions
description is a second muted line under the label in the row, for what the option means or costs. It joins the row's accessible description; the filter goes by the label, and the chip shows the label alone.
As a property
variant="quiet" draws the chips as they are with no shell around them at rest: the fill comes on hover, the focus ring on focus and the shell only while the panel is open. The placeholder takes the surrounding font. The label is not drawn but still names the control, so the host prints the property name.
Chip overflow
maxChips caps the chips drawn in the trigger. The rest collapse into a plain "+N more" count.
States
required means at least one pick. An error replaces the help line, paints the trigger in the danger tone and sets aria-invalid, and a disabled control posts nothing and never holds the form.
Loading and clearable
While loading is set a spinner stands in for the chevron and the panel shows the loading row instead of options. clearable draws an X that clears every pick at once and reports an empty array.
Sizes
size is the same sm, md, lg vocabulary and the same 28, 34, 42 floors as the rest of the control family. The trigger grows past its floor when the chips wrap.
A long list
From virtualScrollThreshold options, 200 by default, the panel draws only the rows near the view and stands the rest in a blank the height of the list. Select all still ticks every option, and the arrows, Home, End, typeahead and the position a screen reader reads count over the whole list.
Read-only
readonly keeps the chips without their remove control and the trigger in the tab order, opens nothing, and the picks still post.
Empty and no matches
emptyMessage is the row for a list that has nothing in it and emptyFilterMessage the row for a search that found nothing; they are two states and two sentences, so "Nothing here yet." is never shown over a long list the reader has just narrowed to none.
In a form
Each picked value posts as its own entry under the host name, the way a native select multiple does.
Events
suiChange carries the full array of picked values. suiOpen fires after the element opens itself, and the cancelable suiClose fires before it closes with a reason of escape, outside or trigger.
Keyboard
| Key | Action |
|---|---|
| ArrowDown ArrowUp | Opens the panel. Once open, moves the active row through the select-all row and the options, skipping disabled ones and wrapping at either end. |
| Enter | On the trigger, opens the panel or toggles the active row. In the search box, toggles the active row. |
| Space | On the trigger, opens the panel or toggles the active row. In the search box it types a space. |
| Home End | Moves to the select-all row, or to the last option. In the search box the caret keeps them. |
| Escape | Closes the panel and returns focus to the trigger. |
| Tab | Closes the panel, then reaches each chip's remove control, labeled "Remove {label}", before moving on. |
| Delete Backspace | On the trigger, with clearable set, clears every pick. |
| Printable characters | On the trigger, jumps the active row to the next label starting with the typed text. In the search box they filter the rows. |
API
<sui-multi-select>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
clearable | clearable | boolean | false | X control that clears every pick at once. Delete or Backspace on the trigger does the same. |
disabled | disabled | boolean | false | Disables the trigger. A disabled control posts nothing and never holds the form. |
emptyFilterMessage | empty-filter-message | stringstring | undefined | Row shown when the filter matched nothing. An empty list and a search that found nothing are different states and one sentence cannot be right about both: "Nothing here yet." is wrong over a list of two hundred the reader has just narrowed to none. | |
emptyMessage | empty-message | stringstring | undefined | Row shown when the option list is empty. | |
error | error | stringstring | undefined | Error message; paints the trigger in the danger tone and sets aria-invalid. | |
filter | filter | boolean | false | Search box inside the panel. It takes focus when the panel opens. |
help | help | stringstring | undefined | Help text under the control. Replaced by error when set. | |
invalid | invalid | boolean | false | Danger border without a message, for a form that draws its summary elsewhere. |
label | label | stringstring | undefined | Label above the control. | |
loading | loading | boolean | false | Shows the options are still on their way: a spinner stands in for the chevron and the panel shows the loading row instead of options. Set it while fetching, clear it when options land. |
loadingMessage | loading-message | stringstring | undefined | Text of the loading row. | |
maxChips | max-chips | number | 3 | Chips drawn in the trigger before the rest collapse into a "+N more" count. |
name | name | stringstring | undefined | Form control name; each picked value posts under it. Reflected: the form entry is built from the content attribute. | |
open | open | boolean | false | Opens the panel. Mutable and the single source of truth: suiOpen announces an open the element performed, and the cancelable suiClose asks before it closes itself on outside click, Escape and Tab. Picking never closes the panel. |
options | property only | SuiMultiSelectEntry[] | [] | Options: strings or numbers, { label, value, disabled, icon, description, render } objects, or { label, items } groups whose items are either. A group draws a header over its rows. icon is a Font Awesome class drawn before the label in the row and on the chip; description is a muted second line under the label in the row only; render draws the row yourself, for an avatar say, and is called with 'option'. Property, not attribute. |
placeholder | placeholder | stringstring | undefined | Text shown while nothing is picked. | |
readOnly | readonly | boolean | false | Display only: the chips can be read but not removed, the trigger keeps its name and its tab stop and opens nothing, and the picks still post. Neither a read-only nor a disabled control holds the form on valueMissing. |
required | required | boolean | false | At least one pick is required for the surrounding form to be valid. |
size | size | "lg" | "md" | "sm" | 'md' | Trigger height: sm 28, md 34 (default), lg 42, from the --control-* tokens. The trigger grows past it when chips wrap. In the quiet variant it is the height of the box that paints on hover and while open. |
tone | tone | "inverse" | undefined | Ink for the quiet variant on the brand chrome. inverse takes the inverse inks, the inverse hover wash and the inverse focus colour, as sui-button's tone of the same name does, so the value reads on a tone="brand" toolbar. Opening the field shows its shell in the card's own inks. The default variant ignores it. | |
value | property only | SuiKey[] | [] | Picked option values, each as its option gives it (a number stays a number). Mutable; updated before suiChange fires. Property, not attribute. |
variant | variant | "default" | "quiet" | 'default' | default draws the field shell. quiet draws the chips as they are, with no border or fill around them at rest, for a record's properties column: the fill comes on hover, the focus ring on focus, and the shell only while the panel is open. The placeholder takes the surrounding font. The label is not drawn but still names the control, so the host prints the property name. Keyboard, events, validation and the form post are the same in both. Anything else falls back to default. |
virtualScrollThreshold | virtual-scroll-threshold | number | 200 | Option count at which the panel starts mounting only the rows near the viewport instead of all of them, so a list of exactly this many is windowed. The select-all row, the keyboard, the ids and the counts a screen reader reads still cover every option. Set it to 0 to keep every row in the DOM whatever the list costs, or to 1 to window a short list under test. |
Events
| Event | Detail | React | Vue | Description |
|---|---|---|---|---|
suiChange | SuiKey[] | onSuiChange | @sui-change | Fires when the picks change; detail is the new value, the full array of picked values as their options give them (empty after a clear). |
suiClose | SuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger" | onSuiClose | @sui-close | Fires before the element closes the panel itself; detail is 'escape' | 'outside' | 'trigger'. Cancelable: preventDefault() keeps it open. Tab reports 'outside', because focus left the control and SuiCloseReason has no member for that. Picking never closes a multi-select. Does not bubble: a preventDefault() in a dialog's own suiClose listener cannot veto this one. |
suiOpen | void | onSuiOpen | @sui-open | Fires after the element opens the panel itself (trigger click, ArrowDown, Enter, Space). Does not bubble, so a dialog around the control never takes it for its own. |
Methods
| Method | Signature | Description |
|---|---|---|
setFocus() | setFocus() => Promise<void> | Moves keyboard focus to the trigger, as el.focus() does. |
Types
SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own. | |||
|---|---|---|---|
| No members. |
SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string. | |||
|---|---|---|---|
| No members. |
SuiMultiSelectEntry typeOne entry of options: a string or a number, an option object, or a group. | |||
|---|---|---|---|
| No members. |
SuiMultiSelectGroup interfaceA group: its header label and the options under it. Groups never nest. | |||
|---|---|---|---|
label | string | ||
items | SuiMultiSelectOptionInput[] | ||
SuiMultiSelectOption interfaceOne option. | |||
|---|---|---|---|
label | string | Text of the row: what the filter and the typeahead match, what a screen reader hears, and what the chip says once picked. | |
value | SuiKey | One of the values value holds, as given: a number stays a number through value, suiChange and a framework binding. The form posts its text. | |
disabled? | boolean | Greys the row out; the arrows and select-all step over it. | |
icon? | string | Font Awesome class drawn before the label, in the row and on the chip. | |
description? | string | A second line under the label in the row, in the muted ink, for what the option means or costs. It joins the row's accessible description; the filter, the typeahead and the chip keep to the label. A row with one is taller, so a list long enough to be windowed should give every option one, or none. | |
render? | (place: SuiSelectRenderPlace) => SuiRenderResult | Draws the row yourself: return a string or an element. The same signature as a sui-select option's, so one options array serves both; here it is only ever called with 'option', since the trigger draws chips, and a chip carries the icon and the label. It is called on every draw. The label still names the row for a screen reader and still drives the filter and the typeahead. A render that throws, as one that builds nodes with the page's document does on a server, draws the label instead. | |
SuiMultiSelectOptionInput typeAn option: a string or a number stands for itself as label and value. | |||
|---|---|---|---|
| No members. |
SuiRenderResult type | |||
|---|---|---|---|
| No members. |
SuiSelectRenderPlace typeWhere an option's render output is drawn: its row in the list, or the trigger once it is the value. | |||
|---|---|---|---|
| No members. |
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.
Color23
Typography1
Spacing and size5
Radius3
Elevation2
Motion3
Every name above is listed with its value and its role in Foundations.