A combobox is a text field with a filtered list of options behind it. Typing
narrows the list; it never completes what you typed, and the reader can only
return values you passed in options. multiple decides where the typing
happens: in single select the field itself is the search box, and in
multi-select the selections fill the field and the search moves to the top of
the popover. ComboboxField is the component to render: a combobox with a
FormField already around it, and it discriminates on
multiple the same way.
Anatomy
Section titled “Anatomy”- Input container the field. In single select the reader types here to filter the list; in multi-select it holds the selections instead.
- Sticker (optional) one selection, shown inside the field in multi-select and removable from there. Under
truncatethe individual dismiss goes, leaving only the clear button. - Clear button (optional) empties every selection at once.
- Caret button opens and closes the popover without typing.
- Popover (when open) the surface holding the searchable results. It takes the width of the field on its own, and caps its own height at 300px.
- Search input (optional, when open) multi-select's own search field, pinned to the top of the popover. Single select has none, because the field itself is the search box.
- List item (when open) one option. It can carry a leading icon and a description of up to three lines; anything longer belongs on the page.
- Checkbox (optional, when open) marks a chosen option in multi-select, so the list says what the field says.
- Empty state (optional, when open) what the list says when the search matches nothing.
- Footer action (optional, when open) an action pinned below the results and unaffected by the search, typically creating the option the reader just typed.
When to use
Section titled “When to use”The popover caps itself at 300 pixels tall and scrolls, which is about ten rows. Past that the reader cannot see the end of the list, and typing is the only thing that brings it back within reach. That is the line a combobox is on the far side of. Below it, filtering charges a keystroke for nothing, and a Select does the same job in one click.
The list length is the whole of the decision. A List Box is for options that have to stay visible, Radio Buttons for a handful of exclusive choices, and Checkboxes for a handful of multiple ones. Where the answer cannot be listed in advance, none of them work: a combobox has no way to return a value that is not already an option, so freeform answers need a text input.
Multi-select can render only the rows in view, with virtualize. It is off by
default and worth turning on somewhere in the thousands.
ComboboxField wraps both modes, and its table documents one
Section titled “ComboboxField wraps both modes, and its table documents one”ComboboxField takes FormField’s props and the combobox’s props in one object,
and it splits multiple into its own branch internally, so multi-select gets
the same label, helper text and validation slots as single select.
Its props table below is generated from the single-select side. The component’s
parameter is a union of two types and there is no single set of combobox field
props to read, so values, onValuesChange, truncate, selectAll,
searchPlaceholder and stableOptionOrder appear in the table typed as
undefined rather than as the props they are in multi-select. Neither table
carries them properly: the combobox table underneath omits every value prop of
both modes for the same reason. Their behavior is described in prose above.
For a combobox with something else in the field slot beside it, hand-compose a FormField instead.
Single and multi-select
Section titled “Single and multi-select”Choose the mode before anything else
Section titled “Choose the mode before anything else”multiple discriminates a union, and the two sides are separate components
behind one name. Single select takes value and onChange; multi-select takes
values and onValuesChange and types value as never, so the compiler
stops you mixing them. The props table below lists neither pair, because it
documents only what the two modes share.
Hold multi-select selections to one line when the field cannot grow
Section titled “Hold multi-select selections to one line when the field cannot grow”Each selection renders as a Sticker inside the field, so
the field gets taller with every pick. truncate keeps it to one line: the
component measures how many stickers fit and replaces the rest with a sticker
counting them.
Truncated stickers lose their dismiss button, so the clear button beside the caret becomes the only way to remove anything. That is the trade for a field that never changes height.
Read “Select all” as “all of these”
Section titled “Read “Select all” as “all of these””selectAll adds a row above the options. It selects every option the filter is
currently showing and deselects only those, leaving any selection the filter has
hidden in place. With a search term typed, the row acts on what the reader can
see rather than on the whole list, and nothing on screen says which.
Selected options float to the top and move the row you clicked
Section titled “Selected options float to the top and move the row you clicked”Multi-select floats selected options to the top of the list by default, so the
row the reader just clicked jumps somewhere else under the pointer. In a filter
value picker, where selections come in runs, that reads as the wrong item having
toggled. stableOptionOrder leaves the options in the order you passed them.
A nested list adds a browsing mode, and typing abandons it
Section titled “A nested list adds a browsing mode, and typing abandons it”options can nest, and a single select whose list contains a nested group picks
up a second mode. The reader descends into a group, a Back row and the left
arrow bring them back out, and closing the popover puts them at the root again.
Typing abandons the tree: the search runs over every leaf in it at once, and each result carries the path it came from as its description. So the reader is either browsing one level or searching all of them, never both. Flat and grouped lists are untouched by any of this, which only appears once a nested group exists.
Filtering
Section titled “Filtering”Index the words people will type
Section titled “Index the words people will type”Filtering runs over three fields of each option: label, description, and
keywords. keywords is never rendered, which makes it the place for synonyms,
abbreviations and the name a field used to have, without any of it reaching the
list.
A nested list adds a fourth: the labels of the groups an option sits under. So typing a parent’s name returns everything inside it, and a group label is worth writing as a search term too.
The match is a substring, so a typo finds nothing
Section titled “The match is a substring, so a typo finds nothing”The match is a substring, not a fuzzy one, so “governemnt” finds no
option. Matching options also keep the order you passed them rather than sorting
by relevance, which means the best match is not necessarily first and a reader
who types two characters is still scanning. Set emptyStateMessage to say what
a reader with no results should do next.
Accessibility
Section titled “Accessibility”Label the input and let downshift do the rest
Section titled “Label the input and let downshift do the rest”Render a ComboboxField with a label, or pass aria-label, or pass labelId
pointing at a label you rendered. The combobox registers its input’s id with the
surrounding FormField, which is what the label’s for
attribute points at, so the field component needs nothing beyond the label
itself.
role="combobox", aria-expanded, aria-controls and aria-activedescendant
all come from downshift and are not props. The component adds one thing
downshift leaves out: aria-selected on selected options, so the list announces
what the field shows.
aria-describedby on the input lists the helper text, and the warning text in a
warning state. It does not list the error text, so an error message is the one
thing beside the field a screen reader will not reach. See
FormField.
Content
Section titled “Content”Distinguish the label in its first few characters
Section titled “Distinguish the label in its first few characters”The first characters a reader types decide whether their option appears at all. Labels that all begin with the same prefix or an internal code defeat that: two keystrokes match everything, and the reader is back to scanning a list too long to scan.
An option’s description is indexed as well as rendered, so it earns its place twice. Keep it to three lines; anything longer belongs on the page rather than in a row the reader is skimming.
Writing has the rules that apply to every label.
ComboboxField first, read with the caveat above: this is the single-select arm
plus the twelve props FormField carries.
| Name | Type |
|---|---|
label |
Renders |
inputId |
ID of the input element. Providing this will automatically link the label to the input. |
helperText |
Renders |
errorText |
Renders |
warningText |
Renders |
labelProps |
|
helperProps |
|
warningProps |
|
errorProps |
|
configField |
Whether to render the form field as a config field. This will render the helper text as a tooltip. |
configFieldInputWidth |
Override the width of the input area when |
rightActions |
Renders actions to the right of the label. |
multiple |
Single-select mode |
value |
Selected value |
onChange |
Callback when selection changes |
values |
|
onValuesChange |
|
searchPlaceholder |
|
truncate |
|
selectAll |
|
stableOptionOrder |
|
size |
Size of the combobox input |
optionsrequired |
Array of options to display in the dropdown Can be flat options, grouped options, or a mix of both |
isOptionsLoading |
If true, the options are currently being loaded |
onInputChange |
Callback when input value changes (for filtering) In single this is the main input, in multi this is the search input In both cases this corresponds to the input the user can type into to filter the options |
onOpenChange |
Callback when dropdown open state changes |
emptyStateMessage |
Custom empty state message when no options match filter |
disabled |
Whether the combobox is disabled |
labelId |
ID of the label for the combobox input |
aria-label |
Aria label for accessibility when no visible label is provided |
placeholder |
Placeholder text when no option is selected |
hasWarning |
Whether the combobox has a warning state |
hasError |
Whether the combobox has an error state |
id |
ID for the combobox input |
portal |
Whether to render the dropdown in a portal (breaks out of overflow containers) |
portalContainerRef |
Reference to the container element for portal rendering. When provided, the dropdown will use this element’s dimensions for positioning. |
trigger |
Optional external trigger (e.g. an “Add” button or icon-only menu opener). When provided, the Combobox clones it with downshift’s toggle props and renders it in place of the default input + caret; the search input moves inside the dropdown. |
embedded |
Render the searchable option list inline without a trigger or floating dropdown. Use when another component already owns the containing popover. When |
footerAction |
THIS IS A BAD PATTERN AND ONLY EXISTS AS A BANDAID. AVOID USING THIS PROP IF POSSIBLE. Optional action button rendered at the bottom of the dropdown, below the option list. Common use is “Load more” pagination, but the label/handler are up to the consumer. Works identically in single- and multi-select. |
The combobox’s own props, which ComboboxField also accepts:
| Name | Type |
|---|---|
size |
Size of the combobox input |
optionsrequired |
Array of options to display in the dropdown Can be flat options, grouped options, or a mix of both |
isOptionsLoading |
If true, the options are currently being loaded |
onInputChange |
Callback when input value changes (for filtering) In single this is the main input, in multi this is the search input In both cases this corresponds to the input the user can type into to filter the options |
onOpenChange |
Callback when dropdown open state changes |
emptyStateMessage |
Custom empty state message when no options match filter |
disabled |
Whether the combobox is disabled |
labelId |
ID of the label for the combobox input |
aria-label |
Aria label for accessibility when no visible label is provided |
placeholder |
Placeholder text when no option is selected |
hasWarning |
Whether the combobox has a warning state |
hasError |
Whether the combobox has an error state |
id |
ID for the combobox input |
portal |
Whether to render the dropdown in a portal (breaks out of overflow containers) |
portalContainerRef |
Reference to the container element for portal rendering. When provided, the dropdown will use this element’s dimensions for positioning. |
trigger |
Optional external trigger (e.g. an “Add” button or icon-only menu opener). When provided, the Combobox clones it with downshift’s toggle props and renders it in place of the default input + caret; the search input moves inside the dropdown. |
embedded |
Render the searchable option list inline without a trigger or floating dropdown. Use when another component already owns the containing popover. When |
footerAction |
THIS IS A BAD PATTERN AND ONLY EXISTS AS A BANDAID. AVOID USING THIS PROP IF POSSIBLE. Optional action button rendered at the bottom of the dropdown, below the option list. Common use is “Load more” pagination, but the label/handler are up to the consumer. Works identically in single- and multi-select. |
Was this page helpful?