Forms & Input
Combobox
AvailableA text input combined with a suggestion list (autocomplete). One of the most complex patterns in the APG.
What Is a Combobox?
A combobox is a UI that combines a text input with a popup list of suggestions that appears as the user types. It is commonly used for search autocomplete, address and state lookups, tagging, and similar interactions.
It is essentially a fusion of a free-text input and a selectable listbox. Because of this, communicating state (whether the popup is open, which option is highlighted) is complex, making it one of the most challenging patterns in the APG.
Why Does Accessibility Matter?
- Keyboard-only users. They need ↓ to move into the suggestions,↑↓ to navigate, Enter to confirm, and Esc to dismiss. Plain
<div>suggestions are mouse-only — keyboard users cannot select them. - Screen reader users. They need to hear "Search states, edit, combobox, collapsed" for the role and state, "X results available" when suggestions appear, and "California, selected, 4 of 12" as they navigate — conveying the count and the highlighted position.
This is achieved through role="combobox" + aria-expanded +aria-controls on the input, role="listbox" on the popup, andaria-activedescendant to communicate the highlighted option.
Live Demo (Recommended Implementation)
A combobox for searching US states. As you type, suggestions are filtered. Try operating it without a mouse — use the arrow keys, Enter, and Esc.
- Alabama
- Alaska
- Arizona
- California
- Colorado
- Florida
- Georgia
- New York
- Oregon
- Texas
- Virginia
- Washington
Try it: type to see suggestions → ↓ to enter the list → ↑ ↓ to navigate → Enter to confirm → Esc to dismiss. Try filtering with 'Cal', 'New', 'Al', etc.
Tip
Focus stays on the input at all times. Instead of moving focus,aria-activedescendant tells assistive technology which option is currently highlighted. This lets users keep typing while selecting from the list. The open/close state is conveyed through aria-expanded and the result count viaaria-live.
Keyboard Interaction
| Key | Action | Priority |
|---|---|---|
| Text input | Filter suggestions and open the popup | Required |
| ↓ | Open the popup / move highlight to the next suggestion | Required |
| ↑ | Move highlight to the previous suggestion | Required |
| Enter | Accept the highlighted suggestion and close | Required |
| Esc | Close the popup | Required |
| Esc (when popup is closed) | Clear the input | Optional |
| Home / End | Move the cursor within the input field (standard text editing) | Optional |
Required WAI-ARIA Roles and Properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Input field | role="combobox" | Indicates a composite widget combining text input with a popup. |
| Input field | aria-expanded="true | false" | Indicates whether the popup is open. Must be updated on every open/close. |
| Input field | aria-controls="listbox-id" | Identifies which popup (listbox) this input controls. |
| Input field | aria-activedescendant="option-id" | Points to the currently highlighted option. Updated on each move; removed when closed. |
| Input field | aria-autocomplete="list" | Indicates that suggestions are provided via a list based on input. |
| Popup | role="listbox" + name | Container for the suggestions. Named via aria-labelledby or similar. |
| Each suggestion | role="option" + unique id + aria-selected | A single suggestion. aria-selected="true" when highlighted. |
| Live region | aria-live="polite" | Announces changes such as the number of available suggestions (optional but recommended). |
Implementation: Recommended Pattern (Good)
Good / Recommended
Give the input role="combobox" and manage aria-expanded/aria-controls/aria-activedescendant. The popup uses role="listbox" > role="option".
Markup:
<label id="cmb-label" for="cmb-input">Search states</label>
<input
id="cmb-input"
type="text"
role="combobox"
aria-expanded="false"
aria-controls="cmb-list"
aria-autocomplete="list"
autocomplete="off"
>
<ul id="cmb-list" role="listbox" aria-labelledby="cmb-label" hidden>
<li role="option" id="cmb-opt-0">Alabama</li>
<li role="option" id="cmb-opt-1">Alaska</li>
<!-- ... -->
</ul>
<!-- Live region for announcing the number of results -->
<p class="sr-only" aria-live="polite"></p>Script for filtering, open/close, highlighting, and selection:
const input = document.getElementById('cmb-input');
const listbox = document.getElementById('cmb-list');
const options = [...listbox.querySelectorAll('[role="option"]')];
let activeIndex = -1;
const visible = () => options.filter((o) => !o.hidden);
const open = () => {
input.setAttribute('aria-expanded', 'true');
listbox.hidden = false;
};
const close = () => {
input.setAttribute('aria-expanded', 'false');
listbox.hidden = true;
activeIndex = -1;
input.removeAttribute('aria-activedescendant');
options.forEach((o) => o.setAttribute('aria-selected', 'false'));
};
// Communicate the currently highlighted option via aria-activedescendant
const setActive = (i) => {
const vis = visible();
options.forEach((o) => o.setAttribute('aria-selected', 'false'));
if (i < 0 || i >= vis.length) {
activeIndex = -1;
input.removeAttribute('aria-activedescendant');
return;
}
activeIndex = i;
vis[i].setAttribute('aria-selected', 'true');
input.setAttribute('aria-activedescendant', vis[i].id);
vis[i].scrollIntoView({ block: 'nearest' });
};
// Filter options based on input and announce the count
const filter = () => {
const q = input.value.trim();
options.forEach((o) => {
o.hidden = q !== '' && !o.textContent.includes(q);
});
return visible().length;
};
input.addEventListener('input', () => {
filter() > 0 ? open() : close();
setActive(-1);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (input.getAttribute('aria-expanded') !== 'true' && filter() > 0) open();
const n = visible().length;
if (!n) return;
setActive(e.key === 'ArrowDown'
? (activeIndex + 1) % n
: (activeIndex - 1 + n) % n);
} else if (e.key === 'Enter' && activeIndex >= 0) {
e.preventDefault();
input.value = visible()[activeIndex].textContent;
close();
} else if (e.key === 'Escape') {
input.getAttribute('aria-expanded') === 'true' ? close() : (input.value = '');
}
});
options.forEach((opt) => opt.addEventListener('click', () => {
input.value = opt.textContent;
close();
input.focus();
}));Note
The key principle is "never move focus away from the input." Option highlighting is handled virtually via aria-activedescendant, and state is conveyed through aria-expanded plus the result count announced viaaria-live. This is how typing and selection work together seamlessly.
Anti-pattern (Bad)
Below is a plain <input> with <div> suggestions.You can click options with a mouse, but keyboard users cannot navigate to them, and screen readers are unaware of the suggestions, count, or selection.
Try it: typing shows suggestions, but pressing ↓ doesn't move to them, and Enter doesn't select anything. Screen readers have no idea suggestions have appeared.
<!-- ❌ Anti-pattern: plain input + div suggestions -->
<input type="text" placeholder="Enter a state">
<div class="suggestions">
<!-- No roles, no ARIA. Can't select options with keyboard -->
<div class="item" onclick="pick(this)">California</div>
<div class="item" onclick="pick(this)">New York</div>
</div>Bad / Avoid
Problems with this implementation:
- Can't select options via keyboard — suggestions are
divelements with no arrow key or focus support. - No roles or state — missing
role="combobox"/aria-expanded/aria-controls, so the open/close state and relationship are not communicated. - No highlight indication — missing
aria-activedescendant/aria-selected, so there is no way to know which option is current. - No result count — missing
aria-live, so "X results available" is never announced.
Implementation Checklist
- The input has role="combobox" with aria-expanded and aria-controls
- The popup uses role="listbox" containing role="option" elements (each with a unique id)
- Highlight is conveyed via aria-activedescendant and aria-selected, updated on each move
- aria-expanded always matches the open/close state
- Down arrow opens suggestions, Up/Down arrows navigate, Enter accepts, Escape closes
- Typing filters suggestions and the count is announced via aria-live (recommended)
- Focus remains on the input field and focus indicators are visible
Source (English):Combobox Pattern — W3C APG(opens in a new tab)