Persona

Combobox

Inputs · 18 props · Updated Sep 22, 2026

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.

  • 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 truncate the 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.

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.

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 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.

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.

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.

NameType
label

ReactNode

Renders <Label> above input

inputId

string

ID of the input element. Providing this will automatically link the label to the input.

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

configField

boolean

Whether to render the form field as a config field. This will render the helper text as a tooltip.

configFieldInputWidth

string

Override the width of the input area when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

rightActions

ReactNode

Renders actions to the right of the label.

multiple

false

Single-select mode

value

T, null

Selected value

onChange

((value: T | null) => void)

Callback when selection changes

values

undefined

onValuesChange

undefined

searchPlaceholder

undefined

truncate

undefined

selectAll

undefined

stableOptionOrder

undefined

size

InputSizes

Size of the combobox input

optionsrequired

ComboboxOptionsOrGroups<T>

Array of options to display in the dropdown Can be flat options, grouped options, or a mix of both

isOptionsLoading

boolean

If true, the options are currently being loaded

onInputChange

((value: string) => void)

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

((isOpen: boolean) => void)

Callback when dropdown open state changes

emptyStateMessage

string

Custom empty state message when no options match filter

disabled

boolean

Whether the combobox is disabled

labelId

string

ID of the label for the combobox input

aria-label

string

Aria label for accessibility when no visible label is provided

placeholder

string

Placeholder text when no option is selected

hasWarning

boolean

Whether the combobox has a warning state

hasError

boolean

Whether the combobox has an error state

id

string

ID for the combobox input

portal

boolean

Whether to render the dropdown in a portal (breaks out of overflow containers)

portalContainerRef

RefObject<HTMLElement>

Reference to the container element for portal rendering. When provided, the dropdown will use this element’s dimensions for positioning.

trigger

ReactElement<unknown, string | JSXElementConstructor<any>>

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

boolean

Render the searchable option list inline without a trigger or floating dropdown. Use when another component already owns the containing popover. When true, the in-panel search uses bordered Input chrome and the embedder owns all surrounding spacing; when false, the floating-dropdown borderless search strip is preserved.

footerAction

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:

NameType
size

InputSizes

Size of the combobox input

optionsrequired

ComboboxOptionsOrGroups<T>

Array of options to display in the dropdown Can be flat options, grouped options, or a mix of both

isOptionsLoading

boolean

If true, the options are currently being loaded

onInputChange

((value: string) => void)

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

((isOpen: boolean) => void)

Callback when dropdown open state changes

emptyStateMessage

string

Custom empty state message when no options match filter

disabled

boolean

Whether the combobox is disabled

labelId

string

ID of the label for the combobox input

aria-label

string

Aria label for accessibility when no visible label is provided

placeholder

string

Placeholder text when no option is selected

hasWarning

boolean

Whether the combobox has a warning state

hasError

boolean

Whether the combobox has an error state

id

string

ID for the combobox input

portal

boolean

Whether to render the dropdown in a portal (breaks out of overflow containers)

portalContainerRef

RefObject<HTMLElement>

Reference to the container element for portal rendering. When provided, the dropdown will use this element’s dimensions for positioning.

trigger

ReactElement<unknown, string | JSXElementConstructor<any>>

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

boolean

Render the searchable option list inline without a trigger or floating dropdown. Use when another component already owns the containing popover. When true, the in-panel search uses bordered Input chrome and the embedder owns all surrounding spacing; when false, the floating-dropdown borderless search strip is preserved.

footerAction

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.