Forms & ControlsFlagship guide
Combobox
A text input combined with a popup listbox that filters/suggests options as the user types.
Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.
Implementation
combobox-pattern.tsx
function ComboboxPattern() {
const [value, setValue] = useState("");
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const inputRef = useRef(null);
const options = FRUITS.filter(f =>
f.toLowerCase().includes(value.toLowerCase())
);
function selectOption(option) {
setValue(option);
setOpen(false);
setActiveIndex(-1);
inputRef.current?.focus(); // focus never leaves the input
}
function onKeyDown(e) {
if (e.key === "ArrowDown") {
e.preventDefault();
setOpen(true);
setActiveIndex(i => Math.min(i + 1, options.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
} else if (e.key === "Enter" && open && activeIndex >= 0) {
e.preventDefault();
selectOption(options[activeIndex]);
} else if (e.key === "Escape" && open) {
e.preventDefault();
setOpen(false);
}
}
return (
<>
<input
ref={inputRef}
role="combobox"
aria-expanded={open}
aria-controls="fruit-listbox"
aria-autocomplete="list"
// Points at the visually-highlighted option WITHOUT moving real
// DOM focus — this is what lets the input keep the caret & keeps
// the screen reader in "edit text" mode while announcing options.
aria-activedescendant={
activeIndex >= 0 ? `fruit-option-${activeIndex}` : undefined
}
value={value}
onChange={(e) => { setValue(e.target.value); setOpen(true); }}
onKeyDown={onKeyDown}
/>
{open && (
<ul id="fruit-listbox" role="listbox" aria-label="Fruits">
{options.map((option, i) => (
<li
key={option}
id={`fruit-option-${i}`}
role="option"
aria-selected={i === activeIndex}
onMouseDown={(e) => { e.preventDefault(); selectOption(option); }}
>
{option}
</li>
))}
</ul>
)}
</>
);
}Live demo
Required roles, states & properties
| Element | Attribute | Why |
|---|---|---|
| Text input | role="combobox" | Identifies the input as a combobox, not a plain textbox, so AT announces "combobox" and exposes expand/collapse state. |
| Text input | aria-expanded | Tells AT whether the suggestion popup is currently open — announced as "collapsed" or "expanded." |
| Text input | aria-controls | Associates the input with the popup listbox it controls, by id. |
| Text input | aria-autocomplete="list" | Tells AT that typing produces a filtered list of suggestions (as opposed to inline text completion). |
| Text input | aria-activedescendant | Points at the id of the currently-highlighted option while DOM focus stays on the input, so the caret and screen reader cursor never leave the edit field. |
| Popup list | role="listbox" | Identifies the popup as a list of selectable options. |
| Each option | role="option" + aria-selected | Identifies each row as selectable and communicates which one is currently active. |
Keyboard interaction model
| Key | Behavior |
|---|---|
| Down Arrow | Opens the popup if closed; otherwise moves the active option to the next item. |
| Up Arrow | Moves the active option to the previous item. |
| Enter | Commits the active option's value into the input and closes the popup. |
| Escape | Closes the popup without changing the input value. |
| Home / End | Moves the active option to the first / last item in the popup. |
| Printable characters | Filters the option list; popup opens automatically. |
Focus management rules
- DOM focus stays on the text input at all times — never moves into the popup.
- The active option is communicated via aria-activedescendant, not real focus.
- Selecting an option (mouse or keyboard) returns focus to the input immediately.
- Closing the popup (Escape or outside click) never moves focus away from the input.
WCAG 2.2 success criteria mapping
| SC | Name | Level | Why it applies |
|---|---|---|---|
| 1.3.1 | Info and Relationships | A | Structure and relationships conveyed visually are also programmatically determinable. |
| 2.1.1 | Keyboard | A | All functionality is operable through a keyboard interface with no specific timing. |
| 2.4.3 | Focus Order | A | Focusable components receive focus in an order that preserves meaning and operability. |
| 3.3.2 | Labels or Instructions | A | Labels or instructions are provided when content requires user input. |
| 4.1.2 | Name, Role, Value | A | For all UI components, name, role, and value are programmatically determinable; states and changes are announced. |