Select
One choice, found fast.
About
Single-choice dropdown: one pick from a list the user already knows. It draws the label, help and error line, and can add a search box and a clear control. Form-associated, so it posts under its name. For free text with suggestions use sui-auto-complete; for several picks use sui-multi-select.
Usage
Basic
The trigger is a button with aria-haspopup="listbox" and aria-expanded, and it points at the active row through aria-activedescendant. With clearable set it advertises Delete through aria-keyshortcuts.
Open with filter
The search box takes focus when the panel opens. A disabled option is announced with aria-disabled and the arrow keys step over it.
Grouped options
A group is { label, items } in the options array and draws an eyebrow header over its rows. The rows stay one list: the arrows walk straight through a header, and a group whose rows all miss the filter loses it.
Icons and avatars
An option's icon is drawn before its label, in the row and in the trigger once picked. render draws the row and the chosen value yourself, called with option or value, while the label still names the row and drives the filter and the typeahead.
Option descriptions
description is a second muted line under the label in the list, for what the option means or costs. It joins the row's accessible description; the filter and the typeahead go by the label, and the trigger shows the label alone.
As a property
variant="quiet" draws the value as text in the surrounding font, its icon before it, with no shell at rest: the fill comes on hover, the focus ring on focus and the shell only while the panel is open. The label is not drawn but still names the control, so the host prints the property name.
States
The field draws the label, the required asterisk and one message line. An error replaces the help line, paints the trigger in the danger tone and sets aria-invalid.
Loading
While loading is set a spinner stands in for the chevron and the panel shows the loading row instead of options; the listbox carries aria-busy. Set it before the fetch and clear it when the options land.
Sizes
size is the same sm, md, lg vocabulary and the same 28, 34, 42 heights as the rest of the control family.
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. The arrows, Home, End, typeahead and the position a screen reader reads all still count over every option; set the threshold to 0 to keep them all in the page.
Read-only
readonly keeps the trigger named and in the tab order, opens nothing and clears nothing, and the value still posts; a disabled select posts nothing. Neither holds the form on valueMissing.
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
The value posts under the host name through ElementInternals. Clearing the required field blocks the submit and the browser names the field from its label.
Numbers as values
An option keeps the type of its value: a number in the options comes back a number in value, in the suiChange detail and through v-model or ngModel, and only the form posts it as text. value="2026" written as an attribute still picks the option 2026.
Events
suiOpen fires after the element opens itself, and suiClose fires before it closes with a reason of pick, escape, outside or trigger; neither bubbles past the select, so a dialog around it never mistakes them for its own. suiChange carries the picked value, and an empty string after a clear.
Keeping the panel open
suiClose is cancelable: calling preventDefault when the reason is pick keeps the panel up so the user can compare rows. Escape and an outside click still close it.
Keyboard
| Key | Action |
|---|---|
| ArrowDown ArrowUp | Opens the panel. Once open, moves the active row, skipping disabled options and wrapping at either end. |
| Enter | Opens the panel, or picks the active row. |
| Space | On the trigger, opens the panel or picks the active row. In the search box it types a space. |
| Home End | Moves to the first enabled row, or to the last row. |
| Escape | Closes the panel and returns focus to the trigger. |
| Tab | Closes the panel and moves on. |
| Delete Backspace | On the trigger, with clearable set, clears the value. |
| Printable characters | On the trigger, once the panel is open, jumps the active row to the next label starting with the typed text. In the search box they filter the rows. |
API
<sui-select>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
clearable | clearable | boolean | false | X control to clear the value. Delete or Backspace on the trigger clears it from the keyboard. |
disabled | disabled | boolean | false | Disables the trigger. A disabled select 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. |
help | help | stringstring | undefined | Help text under the trigger. 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 trigger. | |
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. | |
name | name | stringstring | undefined | Form control name. 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 pick, outside click, Escape and Tab. |
options | property only | SuiSelectEntry[] | [] | 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 in the trigger; description is a muted second line under the label in the row only; render draws the row and the chosen value yourself, for an avatar say, and takes the place it is drawing, 'option' or 'value'. Property, not attribute. |
placeholder | placeholder | stringstring | undefined | Text shown while nothing is picked. | |
readOnly | readonly | boolean | false | Display only: the trigger keeps its name and its place in the tab order, opens nothing and clears nothing, and the value still posts. Neither a read-only nor a disabled select holds the form on valueMissing. |
required | required | boolean | false | A 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. In the quiet variant it is the height of the box that paints on hover and while open; the text keeps the surrounding font. |
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 | value | SuiKeynumber | string | undefined | The picked option's value, as the option gives it (a number stays a number); '' after a clear. Written as an attribute it is text, and it still picks the option whose value reads the same, so value="2" picks the option 2. Mutable; written before suiChange fires. | |
variant | variant | "default" | "quiet" | 'default' | default draws the field shell. quiet draws the value as text in the surrounding font, with its icon before it and no border or fill 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 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 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 | SuiKeynumber | string | onSuiChange | @sui-change | Fires when a value commits; detail is the new value: the option's value as given, or '' 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 'pick' | 'escape' | 'outside' | 'trigger'. Cancelable: preventDefault() keeps it open, so detail === 'pick' is how a consumer keeps the panel up after a choice. Tab reports 'outside', because focus left the control and SuiCloseReason has no member for that. 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 select 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. |
SuiRenderResult type | |||
|---|---|---|---|
| No members. |
SuiSelectEntry typeOne entry of options: a string or a number, an option object, or a group. | |||
|---|---|---|---|
| No members. |
SuiSelectGroup interfaceA group: its header label and the options under it. Groups never nest. | |||
|---|---|---|---|
label | string | ||
items | SuiSelectOptionInput[] | ||
SuiSelectOption interfaceOne option. | |||
|---|---|---|---|
label | string | Text of the row: what the filter and the typeahead match, what a screen reader hears, and what the trigger shows once picked. | |
value | SuiKey | What 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 step over it. | |
icon? | string | Font Awesome class drawn before the label, in the row and in the trigger. | |
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 trigger 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, and the chosen value in the trigger, yourself: return a string or an element for the place asked for. It is called on every draw, so return a new element each time or keep one per place, since one node cannot sit in two places at once. 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. | |
SuiSelectOptionInput typeAn option: a string or a number stands for itself as label and value. | |||
|---|---|---|---|
| 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
Radius2
Elevation2
Motion3
Every name above is listed with its value and its role in Foundations.