Purpose
Let users type or pick a value from a controlled popup while keeping focus on a single text field.
Loading ...
comboboxAn editable text input that controls a popup with suggested options. A combobox pairs free-form typing with a listbox, grid, or tree so people can find and select values efficiently.
Editable input with suggestion popup, arrow navigation, and Enter to confirm selection.
Use arrow keys to explore suggestions and Enter to select.
Let users type or pick a value from a controlled popup while keeping focus on a single text field.
Can control listbox, grid, or tree widgets via aria-haspopup coupled with aria-controls.
Announced as “combobox collapsed/expanded”. Users expect arrow keys to open and traverse results.
Offer live search suggestions for people typing queries, destinations, or commands.
Let users type partial values (countries, tags, products) and confirm with Enter.
Expose thousands of records without forcing people to scroll through long select menus.
Expose an input with role="combobox" (or input[type="text"] + role) that has an accessible name and optional inline help.
Connect the combobox to a role="listbox" | "grid" | "tree" element via aria-controls. Keep the popup in the DOM so assistive tech can discover options.
Manage aria-activedescendant on the input (for true comboboxes) or move DOM focus inside the popup. Only one option should be active at a time.
| Action | Keys | Result |
|---|---|---|
| Open popup | ArrowDown / Alt + ArrowDown / Enter | Opens the popup and moves focus/active item to the first selectable option. |
| Navigate options | ArrowUp / ArrowDown | Moves the active option without leaving the input. Update aria-activedescendant accordingly. |
| Jump to edges | Home / End | Moves to the first or last option within the popup. |
| Commit selection | Enter / Tab | Confirms the currently active option and closes the popup. |
| Dismiss popup | Escape | Closes the popup and restores the previous input value if needed. |
| Typeahead | Printable characters | Filters the popup options and updates aria-busy if you debounce results. |
aria-controlsRequiredReferences the popup element that contains the available options.
aria-expandedRequiredReflects whether the popup is currently visible. Toggle between true and false when opening.
aria-haspopupRequiredCommunicates the popup type (listbox, grid, tree). Screen readers adjust commands accordingly.
aria-activedescendantOptionalPoints to the id of the option that is currently highlighted while focus stays on the input.
aria-autocompleteOptionalUse "list", "both", or "inline" to describe how suggestions behave as the user types.
aria-requiredOptionalMark the combobox required and announce validation feedback via aria-invalid and aria-describedby.
aria-disabledOptionalWhen true, prevents expansion and selection. Always pair with a disabled visual state.
Keep focus on the input element. Drive option highlighting with aria-activedescendant so screen readers stay anchored.
Maintain DOM ownership of the popup even when hidden (use CSS to hide). Removing it breaks assistive tech references.
Constrain popup width to match the input or ensure offset is announced via aria-owns/aria-controls if detached.
If results load asynchronously, set aria-busy on the popup region and provide polite status text.
Ensure pointer interactions mirror keyboard behavior (hover should not steal focus without keyboard confirmation).
Input keeps focus while aria-activedescendant follows the highlighted option.
<label for="country-combobox" class="sr-only">Country</label>
<div class="combobox">
<input
id="country-combobox"
type="text"
role="combobox"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="country-listbox"
aria-autocomplete="list"
autocomplete="off"
/>
<ul id="country-listbox" role="listbox" class="hidden">
<li id="country-option-1" role="option">Canada</li>
<li id="country-option-2" role="option">Germany</li>
<li id="country-option-3" role="option">Japan</li>
</ul>
</div>State-driven popup that supports keyboard filtering and commits a selected option.
import { useMemo, useState } from 'react'
const countries = ['Canada', 'Germany', 'Japan', 'Kenya', 'Peru']
export function CountryCombobox() {
const [value, setValue] = useState('')
const [expanded, setExpanded] = useState(false)
const [activeIndex, setActiveIndex] = useState<number | null>(null)
const filtered = useMemo(
() => countries.filter((item) => item.toLowerCase().includes(value.toLowerCase())),
[value]
)
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (!expanded && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
event.preventDefault()
setExpanded(true)
setActiveIndex(0)
return
}
if (event.key === 'ArrowDown') {
event.preventDefault()
setActiveIndex((prev) => Math.min((prev ?? -1) + 1, filtered.length - 1))
} else if (event.key === 'ArrowUp') {
event.preventDefault()
setActiveIndex((prev) => Math.max((prev ?? filtered.length) - 1, 0))
} else if (event.key === 'Enter' && activeIndex !== null) {
event.preventDefault()
setValue(filtered[activeIndex])
setExpanded(false)
} else if (event.key === 'Escape') {
setExpanded(false)
setActiveIndex(null)
}
}
const activeId = activeIndex === null ? undefined : `country-${activeIndex}`
return (
<div className="relative">
<label htmlFor="country" className="sr-only">
Country
</label>
<input
id="country"
role="combobox"
aria-expanded={expanded}
aria-controls="country-listbox"
aria-haspopup="listbox"
aria-activedescendant={activeId}
value={value}
onChange={(event) => {
setValue(event.target.value)
setExpanded(true)
}}
onKeyDown={handleKeyDown}
onBlur={() => setExpanded(false)}
/>
<ul
id="country-listbox"
role="listbox"
className={expanded ? 'absolute top-full left-0 w-full bg-white shadow-lg' : 'hidden'}
>
{filtered.map((item, index) => (
<li
id={`country-${index}`}
role="option"
key={item}
aria-selected={index === activeIndex}
onMouseDown={() => {
setValue(item)
setExpanded(false)
}}
>
{item}
</li>
))}
</ul>
</div>
)
}Never move DOM focus into the popup. Use aria-activedescendant so assistive tech stays in the input.
Provide inline helper text that documents arrow keys, Enter to select, and Escape to close.
Do not flood the popup with hundreds of options. Paginate or virtualize while updating aria-rowcount.
Keep the text field in sync with the confirmed option to avoid confusion for screen reader users.
The input does not reference the popup element. Screen readers cannot reach the options.
Set aria-controls to the id of the listbox/grid/tree that contains the suggestions.
<input role="combobox" aria-expanded="true">
<ul role="listbox" id="results"></ul><input role="combobox" aria-expanded="true" aria-controls="results">
<ul role="listbox" id="results"></ul>Focus jumps from the input into the list of options, so typing no longer updates suggestions.
Keep focus on the combobox and drive highlighting with aria-activedescendant.
Default popup type that contains selectable option elements.
Learn moreIndividual option that appears inside a listbox or tree.
Learn moreNative input element that a combobox enhances with suggestions.
Learn moreStructured popup used for spreadsheet-like comboboxes.
Learn more