Combobox

User Experience (UX)

How a combobox behaves for every person, input and setting, from the ARIA pattern and the keyboard to filtering, typed values, forms and the on-screen keyboard.

Now let's look at how a combobox behaves: how assistive technologies read it, what each key does, how it filters, what happens to text that isn't an option, and how it works in a form and on a phone.

Accessible

Use a text field with the combobox role

A combobox is an <input> with role="combobox", and a listbox of options that it controls. We follow the APG's Editable Combobox With List Autocomplete, the kind the combobox pattern calls list autocomplete with manual selection, where "the character string the user has typed will become the value of the combobox unless the user selects a value in the popup". The text field carries the attributes that make it one:

  • aria-autocomplete="list" tells screen readers that typing narrows a list of suggestions, rather than completing the text in place (WAI-ARIA 1.2).
  • aria-expanded says whether the panel is open, and aria-controls points at it, so a screen reader can say there's a list and how to reach it.
  • aria-activedescendant points at the highlighted option while the panel is open, which is how screen readers read the option without focus leaving the text.

The pattern lists three other kinds, and we turned them all down. With no autocomplete, the list shows the same options whatever people type, which doesn't help them find one in a long list. With automatic selection, "the first suggestion is automatically highlighted as selected", so Enter and blur pick it, and people who type "Fra" for a place called Fra get France. With inline autocomplete, the rest of the first match is typed into the field as selected text, which gets in the way of people typing with an input method, like Japanese, and of screen readers that read the selection out on every key.

Name every combobox

Give every combobox a visible label, from a Field or a <label for>, since the text field is an <input> and the label names it the same way it names an Input (WCAG 3.3.2). The panel takes the same name, so people hear "Country, listbox" when they reach it. The toggle has a name of its own, "Suggestions" by default, which voice control users can say, while its aria-expanded says whether the list is showing.

Keep focus in the text

Focus stays in the text field the whole time, while the arrow keys move a highlight through the options. That way people can keep typing to narrow the list, whatever's highlighted, and screen readers hear each highlighted option through aria-activedescendant. A press on an option doesn't take focus either, and neither does a press on the toggle, which is out of the tab order, as in the APG's example.

The highlight is a 2px outline in the focus colour, the same as the field's focus ring, so people who use the keyboard can see where they are in the list.

Contrast and size

The text, the placeholder and "No results" have at least 4.5:1 against the page, and the border, the chevron and the focus ring at least 3:1 (WCAG 1.4.3, 1.4.11). The toggle and each tag's remove button are at least 24 × 24px to press (WCAG 2.5.8), and on touch screens they and the options reach 44px tall, which is what WCAG 2.5.5 asks for at level AAA.

Predictable

Keyboard

Every key works without a pointer (WCAG 2.1.1). Letters, Space, Home, End, ArrowLeft and ArrowRight edit the text, as they do in any text field, and the rest work the list:

KeyWith the panel closedWith the panel open
A letterTypes it, filters the list and opens itTypes it and filters the list, with nothing highlighted
ArrowDownOpens it on the first optionMoves the highlight down, skipping disabled options, and stops at the last
Alt+ArrowDownOpens it with nothing highlightedThe same as ArrowDown
ArrowUpOpens it on the last optionMoves the highlight up, and stops at the first
Alt+ArrowUpNothingCloses it, and keeps the text
PageDown, PageUpNothingMoves the highlight ten options, as on the Select
EnterSettles the text, then submits the formPicks the highlighted option and closes it, or with multiple, toggles it and stays open. With nothing highlighted, settles the text
TabSettles the text and moves onPicks the highlighted option, closes it and moves on. With multiple, settles the text
EscapeClears the text, and the value too for a single valueCloses it, and keeps the text
BackspaceDeletes text. With multiple and no text, removes the last valueDeletes text

Both presses of Escape stop there, so a Dialog around the Combobox stays open, and only once there's nothing left to clear does Escape reach it. That's the APG's example, where Escape "closes" the list if it's showing, and "clears the textbox" if it isn't. "Settles the text" is what Decide what happens to text that isn't an option describes.

Here's where it differs from the Select, whose control is a button, so its keys don't have text to edit:

KeySelectCombobox
A letterJumps to the option that starts with itTypes it, and filters the list
SpaceOpens the panel, or picksTypes a space
Home, EndMove to the first or last optionMove the caret
Enter, closedOpens the panelSettles the text and submits the form
Escape, closedGoes on to the pageClears the text
BackspaceRemoves the last value, with multipleDeletes text, then removes the last value

Open it when people ask for it

The panel opens when people type, press ArrowDown or ArrowUp, or press the toggle, and not when the text field gets focus. People tab through a form to reach the field after it, and a list that opens on every stop covers the fields below it, which is why React Aria's ComboBox opens on input by default too.

It closes when people pick an option, unless they're picking several, press Escape, leave the field or press anywhere outside it. A press in the text, while the panel is open, only moves the caret, so people can fix a typo without losing the list.

Filter as people type

We recommend matching any part of an option's label, ignoring case and accents, so "zealand" finds New Zealand and "cote" finds Côte d'Ivoire. People often remember a word from the middle of a name, and plenty of names start with words like "Republic of" or "Saint", so matching only the start would hide what they're after. Accents are the same: people type on keyboards that don't have them, and a list that wants "Zü" to find Zürich fails them. React Aria's useFilter does the same with its contains and sensitivity: "base".

Matching"zea" finds New Zealand"fra" finds FranceWhen it's best
Any partYesYesNames people might remember a word of, which is most lists
The startNoYesCodes and lists people type from the start, like airports

Groups with no match hide along with their headings, and separators with nothing on one side of them do too. When your list needs to match another way, like on a code as well as a label, give it a filter of your own.

Don't pick an option for people

Typing never highlights an option, so Enter and blur never pick one people didn't choose. They press ArrowDown to move into the list, or press an option, and only then does it become the value. That's what keeps "Fra" from turning into France on its way to the form, and what makes text that isn't an option possible at all.

Say when nothing matches

When nothing matches, the panel stays open and says "No results", so people know the list is empty rather than broken. It isn't an option, so it doesn't take the highlight, and since a listbox may only hold options and groups, it's hidden from assistive technologies, which hear it as a status message instead (WCAG 4.1.3). With creatable, the create option takes its place.

Announce the count once typing pauses

As people type, a screen reader can't see the list grow or shrink, so the Combobox announces how many options match, like "3 results". It waits until typing pauses for 1400ms, the same as GOV.UK's accessible-autocomplete, since a count on every key would talk over the letters people hear as they type. With no match, it announces "No results", or the create option's label.

Decide what happens to text that isn't an option

People type, then leave. When they press Tab, press Enter on a closed panel or move focus away, the Combobox settles the text:

  • Text that's an option's label, ignoring case and accents, picks that option, so "zurich" becomes Zürich. It has to be an option the panel lists for that text, so a filter of your own that leaves an option out keeps it from being picked too.
  • Empty text clears the value.
  • Any other text goes back to the picked option's label, or to empty, and the value doesn't change.

That last case is the one to watch. A text field that keeps "Fra" on screen, while the form submits France, or nothing, tells people one thing and sends another. So unless you turn on one of the props below, text that isn't an option doesn't stay.

Escape works the other way. Its first press closes the panel and leaves the text as it is, so people can keep editing, and its second clears the text, and the value too for a single value, the way the APG's example does.

Keep text people type, when you allow it

Some fields take values that aren't in the list, like a city you don't ship to yet, or a new tag. Two props allow it, and both are off by default, since a value nobody can pick again is often a typo:

  • allowCustomValue keeps the text as it was typed, as the value, when people leave the field. It's for one value, the way Ark UI names it, and like React Aria's allowsCustomValue and Material UI's freeSolo.
  • creatable ends the list with an option like 'Create "Lyon"', which people pick like any other. It fires create with the text, so your code can add it to the list, the pattern Material UI calls "Creatable".

Type one that isn't in the list, and it's kept.

Austria
Belgium
Côte d'Ivoire
Curaçao
Denmark
Finland
France
Germany
Iceland
Ireland
Italy
Netherlands
New Zealand
Norway
Portugal
Réunion
São Tomé and Príncipe
Spain
Sweden
Switzerland
Türkiye
United Kingdom
<Field label="Destination" hint="Type one that isn't in the list, and it's kept.">
  <Combobox id="destination" name="destination" options={countries} allowCustomValue placeholder="Type a country or a city" />
</Field>
Accessibility
Accessibility
CSS
Design tokens
Forms
Performance
<Field label="Topics">
  <Combobox id="topics" name="topics" multiple creatable options={topics} placeholder="Type a topic" onCreate={addTopic} />
</Field>

Here's what the form gets under the Combobox's name in each case:

What people didOne valueWith multiple
Picked an optionIts value, like fr, not its labelEach one's value
Typed text, with allowCustomValueThe text, without spaces at its endsNothing: it's ignored
Picked the create optionThe textThe text, along with the others
Typed text, with creatable, without picking the create optionThe value it had beforeThe values it had before
Typed text, with both propsThe text, kept on blur, or created from the create optionNothing new, unless it's created
Typed text, with neither propThe value it had beforeThe values it had before
Picked nothingNothingNothing

Text that's the same as an option's value, but not its label, like "es" for Spain, would submit as that option, so neither prop keeps it or offers to create it: it goes back like any other text. So give your options values people won't type as a new one, like IDs or codes.

With multiple, allowCustomValue does nothing, and the reference implementation warns you during development. Turning whatever people type into a value, one after another, is what a Tags Input does, so new values come only from the create option there.

Several values, or a Tags Input

With multiple, a Combobox picks several options, each a tag, the way the Select's multiple does. React Aria, Ark UI, Base UI and Material UI all put several values on the combobox too. It's still a list people pick from, so the values are options, and new ones come one at a time through creatable.

A Tags Input is another component, where people type any text and a comma, Enter or a paste splits it into tags they can edit, like email addresses or keywords. Mantine and Ark UI make it a component of its own, even where it's built on their combobox, and so do we, since its keys, its value and its validation all work another way.

Remove a value by pointer and by keyboard

With multiple, each tag has a real <button> to remove it, named after its value, like "Remove France", so voice control users can say it and screen readers read it. Once a value is removed, focus goes to the next tag's button, or back to the text when there's none, so keyboard users never fall back to the top of the page, and screen readers hear "France removed" (WCAG 4.1.3). Backspace in empty text removes the last value too, as people expect from a field of tags.

Let the form do its work

A hidden native <select> is always in the page, so the Combobox takes part in its form the way a Select does: it submits under its name, blocks a submit while it's required and empty, and goes back to its first value when the form resets, with that value's text. The <select> holds an option for each value people typed, so those submit and reset too. The text field has no name, since what people are typing isn't the value until it's picked or kept.

Austria
Belgium
Côte d'Ivoire
Curaçao
Denmark
Finland
France
Germany
Iceland
Ireland
Italy
Netherlands
New Zealand
Norway
Portugal
Réunion
São Tomé and Príncipe
Spain
Sweden
Switzerland
Türkiye
United Kingdom

<form onSubmit={send}>
  <Field label="Country" required>
    <Combobox id="country" name="country" options={countries} defaultValue="fr" />
  </Field>
  <Button type="submit" color="primary">Send</Button>
  <Button type="reset" variant="outline">Reset</Button>
</form>

When a missing value blocks a submit, focus goes to the text field, where the browser's message points. Enter in a closed Combobox settles the text first, then submits the form the way Enter in any text field does, so the form always gets the value people see.

Render it inside a Dialog

Render a Combobox inside the Dialog it belongs to, rather than outside it, so its panel opens in the top layer above the Dialog, and the Dialog's focus trap includes it. Escape closes the panel first, then clears the text, and only then closes the Dialog, so people never lose a form to one press too many.

Edit address

Austria
Belgium
Côte d'Ivoire
Curaçao
Denmark
Finland
France
Germany
Iceland
Ireland
Italy
Netherlands
New Zealand
Norway
Portugal
Réunion
São Tomé and Príncipe
Spain
Sweden
Switzerland
Türkiye
United Kingdom
<Dialog id="address" open={open} title="Edit address" onClose={() => setOpen(false)}>
  <Field label="Country">
    <Combobox id="country" name="country" options={countries} defaultValue="fr" />
  </Field>
</Dialog>

Every state

Closed

The field shows the picked option's label, or the placeholder, or with multiple, the tags under empty text. The chevron points down.

Open

The panel shows every option when it opens on a picked value, so people see what else they could pick, and only the matches once they type. The highlight shows where the arrow keys are, and a check mark shows what's picked.

Disabled

A disabled Combobox is dimmed, can't take focus or open, and leaves its form, like any disabled control. Try to explain why it's disabled near it, since people can't reach it to find out.

Read-only

A read-only Combobox keeps its value, its contrast and its place in the tab order, and submits with its form, but doesn't open, and its tags lose their remove buttons.

Invalid

While its Field has an error, the text field has aria-invalid="true" and is described by the message, so screen readers read the error with the field, and its border takes the error colour.

Long and empty

Long labels wrap in the panel, rather than widening it past the field, and in a tag they're cut short with an ellipsis. An empty list says "No results", and a list that's still empty before people type is one to fill in, since a combobox with nothing to suggest is a text field.

Adaptive

Make room for the on-screen keyboard

On a phone, the on-screen keyboard comes up as soon as the text field has focus, and covers a large part of the screen. It shrinks the visual viewport, but not the layout viewport, and CSS anchor positioning works against the layout one, so a panel placed below the field can open behind the keyboard. So while the keyboard is up, the Combobox places the panel itself, in the part of the screen the keyboard leaves, from the Visual Viewport API, above the field when there's more room there, and it goes back to anchor positioning once the keyboard closes.

There's no native picker to fall back to, as the Select has. A <select> can't take typed text, and the text field that brings up the keyboard is the whole point. We test the scripted placement with a stand-in for the visual viewport, and the rest in a browser's emulation, which has no keyboard, so check it on a real phone too.

Let autofill help where it should

The text field has autocomplete="off" by default, since a browser's own list of past entries opens over the panel and hides the options. Where the Combobox asks people about themselves, like the country they live in, give it the field's name instead, like autocomplete="country-name", so browsers and assistive technologies can fill it in (WCAG 1.3.5). When autofill types an option's label, the Combobox picks that option.

The text field also has spellcheck="false" and autocapitalize="off", since names in a list, like Curaçao, aren't dictionary words to underline, and a phone that capitalises the first letter would get in the way of people searching for a word in the middle of a name.

Pointer, touch and pen

The Combobox follows the Select's rules for each kind of pointer, from the shared styles:

  • Hover styles only apply to pointers that can actually hover (@media (hover: hover)), otherwise a tap would leave the field or an option looking hovered.
  • On touch screens, the toggle and each remove button reach past what you see to a hit area at least 44px tall, and each option is at least 44px tall (WCAG 2.5.5). Rows of tags sit far enough apart that those hit areas don't cover each other.
  • On touch screens, the text is at least 16px, whatever the size, since Safari on iOS zooms in when a text field with smaller text gets focus, as with the Input.

Small screens

The panel is as wide as the field, never wider than the screen less 32px, and never taller than 16rem or the screen less 32px, and it scrolls inside that. It flips above the field when there's no room below, and when neither side has room, it shrinks on the bigger one, as the Popover does. So the field and the panel fit a 320px-wide screen, and a 1280px one at 400% zoom, without scrolling sideways (WCAG 1.4.10). Browsers without CSS anchor positioning get a script that does the same, the Select's, which is also the one that places the panel above an on-screen keyboard.

Motion

The chevron turns and the panel fades in over 150ms, unless people have asked for less motion (WCAG 2.3.3). Filtering doesn't animate, since the list changes with every key and movement there would only get in the way of reading it.

More contrast

When someone asks for more contrast (prefers-contrast: more), as with Increase contrast on macOS and iOS, the borders of the field and the panel are drawn in the text colour at full strength, rather than at 55% and 20%.

Forced colours

Windows contrast themes replace every colour with the user's own, and drop backgrounds and shadows. The focus ring, the highlighted option and a focused remove button are drawn with Highlight, the panel and each tag get a border in CanvasText, and a disabled field or option and the "No results" text use GrayText.

Colour scheme

The panel's background is Canvas, and the text reads --color--neutral-text, which uses light-dark(). The borders, tags and highlights mix that text colour, so the Combobox follows color-scheme and adapts to your light and dark themes on its own, as long as they set it.

Text size and spacing

Heights are minimums in rem, and tags wrap onto more rows, so the field grows with its text instead of clipping it (WCAG 1.4.4). Text longer than the field scrolls inside it, as in any text field, while options wrap and tags end in an ellipsis, as Long and empty explains.

Languages and direction

Every word the Combobox says comes through a prop, so it can be translated: emptyLabel, resultsLabel, createLabel, toggleLabel, removeLabel and removedLabel. It uses logical properties throughout, so it mirrors in right-to-left layouts.

The default filter ignores case and the accents of the Latin, Greek and Cyrillic alphabets, so "zur" finds "Zürich". It keeps every other combining mark, since in scripts like Devanagari and Thai those marks are vowels, and "कताब" is a different word from "किताब". So it doesn't list "किताब" for "कताब", and leaving the field doesn't pick it. When your languages need another comparison, pass a filter of your own, which decides what the panel lists, and leaving the field still picks only an option the panel lists.

UX Checklist

RuleLevelScopeRequirementBasisCheck
combobox/editable-patternMustComponentThe text field has the combobox role, aria-autocomplete="list", aria-expanded and aria-controls, which points at a listboxAPGUnit test, ARIA snapshot
combobox/focus-in-textMustComponentFocus stays in the text field, and aria-activedescendant points at the highlighted option while the panel is openAPGUnit test
combobox/nameMustUsageIt has a visible label, from a Field or a <label for>WCAG 1.3.1, 3.3.2 (A)axe label, Review
combobox/keyboardMustComponentEvery key in the contract does what it says, and nothing needs a pointerWCAG 2.1.1 (A)Unit test, Keyboard
combobox/typing-editsMustComponentLetters, Space, Home, End and the side arrows edit the text, and never move the highlightAPGUnit test
combobox/manual-selectionMustComponentTyping highlights nothing, so Enter and blur never pick an option people didn't chooseOpen ComponentsUnit test
combobox/open-on-requestShouldComponentFocus alone doesn't open the panel, while typing, the arrow keys and the toggle doOpen ComponentsUnit test
combobox/press-outsideShouldComponentA press outside the field closes the panel, and a press in its text doesn'tOpen ComponentsUnit test
combobox/escapeMustBothEscape closes the panel, then clears the text, and goes no further, so a Dialog around it stays openAPGUnit test
combobox/filterShouldComponentThe default filter matches any part of a label, ignoring case and accents, and hides groups and separators with no matchOpen ComponentsUnit test
combobox/no-resultsMustComponentWith no match, the panel says so, in text that isn't an option, and screen readers hear itWCAG 4.1.3 (AA)Unit test, axe aria-required-children
combobox/result-countShouldComponentThe number of matches is announced once typing pauses, never on every keyWCAG 4.1.3 (AA)Unit test, Screen reader
combobox/settle-textMustComponentText that's the label of an option the panel lists picks it, and other text goes back to the value's text when people leave, unless a prop keeps itOpen ComponentsUnit test
combobox/opt-in-valuesMustBothTyped values are kept only with allowCustomValue or creatable, which are off by default, and the page says what each submitsOpen ComponentsUnit test, Review
combobox/native-formMustComponentIt submits under its name, an option as its value and a typed value as typed, blocks a submit while required and empty, and resets with its formHTMLUnit test
combobox/removable-tagsMustComponentWith multiple, each tag has a named button that removes it, focus moves on to the next one, and Backspace in empty text removes the lastWCAG 2.1.1, 4.1.2 (A)Unit test
combobox/announce-removalShouldComponentRemoving a value is announcedWCAG 4.1.3 (AA)Unit test
combobox/disabled-readonlyMustComponentDisabled leaves the form and can't open, and read-only keeps the value in the form and can't change itHTMLUnit test
combobox/keyboard-fitShouldComponentWhile an on-screen keyboard is up, the panel opens in the part of the screen it leavesOpen ComponentsUnit test, Emulation
combobox/autocompleteShouldUsageautocomplete names the field's purpose where it asks people about themselves, and stays off elsewhereWCAG 1.3.5 (AA)Review

FAQ

Combobox or Select?

We recommend the Select when the list is short enough to scan, since it's one press to open and one to pick, and phones show their own picker for it. Reach for the Combobox when the list is long enough that people would rather type than scroll, like countries or people, or when they need to add a value that isn't in it.

When you need to…Use
Pick one option out of a few, which fit on the screenRadio Group
Pick one option, or several, out of a list people scanSelect
Type to filter a long list, and pick one or severalCombobox
Keep a value that isn't in the listCombobox, with allowCustomValue or creatable
Type any number of free values, like email addressesTags Input
Copyright © 2026