User Experience (UX)
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-expandedsays whether the panel is open, andaria-controlspoints at it, so a screen reader can say there's a list and how to reach it.aria-activedescendantpoints 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:
| Key | With the panel closed | With the panel open |
|---|---|---|
| A letter | Types it, filters the list and opens it | Types it and filters the list, with nothing highlighted |
ArrowDown | Opens it on the first option | Moves the highlight down, skipping disabled options, and stops at the last |
Alt+ArrowDown | Opens it with nothing highlighted | The same as ArrowDown |
ArrowUp | Opens it on the last option | Moves the highlight up, and stops at the first |
Alt+ArrowUp | Nothing | Closes it, and keeps the text |
PageDown, PageUp | Nothing | Moves the highlight ten options, as on the Select |
Enter | Settles the text, then submits the form | Picks the highlighted option and closes it, or with multiple, toggles it and stays open. With nothing highlighted, settles the text |
Tab | Settles the text and moves on | Picks the highlighted option, closes it and moves on. With multiple, settles the text |
Escape | Clears the text, and the value too for a single value | Closes it, and keeps the text |
Backspace | Deletes text. With multiple and no text, removes the last value | Deletes 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:
| Key | Select | Combobox |
|---|---|---|
| A letter | Jumps to the option that starts with it | Types it, and filters the list |
Space | Opens the panel, or picks | Types a space |
Home, End | Move to the first or last option | Move the caret |
Enter, closed | Opens the panel | Settles the text and submits the form |
Escape, closed | Goes on to the page | Clears the text |
Backspace | Removes the last value, with multiple | Deletes 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 France | When it's best |
|---|---|---|---|
| Any part | Yes | Yes | Names people might remember a word of, which is most lists |
| The start | No | Yes | Codes 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
filterof 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:
allowCustomValuekeeps 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'sallowsCustomValueand Material UI'sfreeSolo.creatableends the list with an option like 'Create "Lyon"', which people pick like any other. It firescreatewith 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.
<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>
<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 did | One value | With multiple |
|---|---|---|
| Picked an option | Its value, like fr, not its label | Each one's value |
Typed text, with allowCustomValue | The text, without spaces at its ends | Nothing: it's ignored |
| Picked the create option | The text | The text, along with the others |
Typed text, with creatable, without picking the create option | The value it had before | The values it had before |
| Typed text, with both props | The text, kept on blur, or created from the create option | Nothing new, unless it's created |
| Typed text, with neither prop | The value it had before | The values it had before |
| Picked nothing | Nothing | Nothing |
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.
<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.
<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
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
combobox/editable-pattern | Must | Component | The text field has the combobox role, aria-autocomplete="list", aria-expanded and aria-controls, which points at a listbox | APG | Unit test, ARIA snapshot |
combobox/focus-in-text | Must | Component | Focus stays in the text field, and aria-activedescendant points at the highlighted option while the panel is open | APG | Unit test |
combobox/name | Must | Usage | It has a visible label, from a Field or a <label for> | WCAG 1.3.1, 3.3.2 (A) | axe label, Review |
combobox/keyboard | Must | Component | Every key in the contract does what it says, and nothing needs a pointer | WCAG 2.1.1 (A) | Unit test, Keyboard |
combobox/typing-edits | Must | Component | Letters, Space, Home, End and the side arrows edit the text, and never move the highlight | APG | Unit test |
combobox/manual-selection | Must | Component | Typing highlights nothing, so Enter and blur never pick an option people didn't choose | Open Components | Unit test |
combobox/open-on-request | Should | Component | Focus alone doesn't open the panel, while typing, the arrow keys and the toggle do | Open Components | Unit test |
combobox/press-outside | Should | Component | A press outside the field closes the panel, and a press in its text doesn't | Open Components | Unit test |
combobox/escape | Must | Both | Escape closes the panel, then clears the text, and goes no further, so a Dialog around it stays open | APG | Unit test |
combobox/filter | Should | Component | The default filter matches any part of a label, ignoring case and accents, and hides groups and separators with no match | Open Components | Unit test |
combobox/no-results | Must | Component | With no match, the panel says so, in text that isn't an option, and screen readers hear it | WCAG 4.1.3 (AA) | Unit test, axe aria-required-children |
combobox/result-count | Should | Component | The number of matches is announced once typing pauses, never on every key | WCAG 4.1.3 (AA) | Unit test, Screen reader |
combobox/settle-text | Must | Component | Text 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 it | Open Components | Unit test |
combobox/opt-in-values | Must | Both | Typed values are kept only with allowCustomValue or creatable, which are off by default, and the page says what each submits | Open Components | Unit test, Review |
combobox/native-form | Must | Component | It 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 form | HTML | Unit test |
combobox/removable-tags | Must | Component | With multiple, each tag has a named button that removes it, focus moves on to the next one, and Backspace in empty text removes the last | WCAG 2.1.1, 4.1.2 (A) | Unit test |
combobox/announce-removal | Should | Component | Removing a value is announced | WCAG 4.1.3 (AA) | Unit test |
combobox/disabled-readonly | Must | Component | Disabled leaves the form and can't open, and read-only keeps the value in the form and can't change it | HTML | Unit test |
combobox/keyboard-fit | Should | Component | While an on-screen keyboard is up, the panel opens in the part of the screen it leaves | Open Components | Unit test, Emulation |
combobox/autocomplete | Should | Usage | autocomplete names the field's purpose where it asks people about themselves, and stays off elsewhere | WCAG 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 screen | Radio Group |
| Pick one option, or several, out of a list people scan | Select |
| Type to filter a long list, and pick one or several | Combobox |
| Keep a value that isn't in the list | Combobox, with allowCustomValue or creatable |
| Type any number of free values, like email addresses | Tags Input |
Enter picks only what's highlighted, and ArrowDown is one press away.