Forms & Input
Listbox
AvailableSelecting from a list of options. Supports single and multi-select with arrow key navigation.
What is a Listbox?
A listbox is a UI that displays a list of options for users to choose from. While it may look like an ordinary list, it differs in that it represents a collection of selectable itemsand tracks which item is currently selected.
This page covers the single-select listbox. It works well when the number of options is small and can remain visible at all times. If the list needs to open and close, consider using a Combobox instead.
Why Does Accessibility Matter?
- Keyboard-only users need to navigate items with ↑↓and jump to either end with Home/End. With
<div>+onclick, arrow keys don't work, making selection impossible. - Screen reader users hear announcements like "Choose your favorite fruit, listbox, Apple, selected, 1 of 6", conveying the label, role, selection state, and position. Plain
<div>elements don't communicate that this is a selectable list.
The solution is the role="listbox" > role="option" structure, combined with aria-selected and keyboard focus management(either roving tabindex or aria-activedescendant).
Live Demo (Recommended Implementation)
This is a listbox for choosing a single fruit. Focus the list, then try navigating with the arrow keys.
- Apple
- Orange
- Grape
- Peach
- Strawberry
- Melon
Try it: Tab to focus the list → ↑ ↓ to move the selection, Home / End to jump to the first or last item. You can also click to select.
Tip
This demo uses the aria-activedescendant approach. Focus stays on the list (<ul>), and only the attribute indicating the current option changes. You can also implement this with the "roving tabindex" approach, where tabindex is moved between individual options.
Keyboard Interaction
| Key | Action | Priority |
|---|---|---|
| Tab | Move focus to / away from the listbox | Required |
| ↓ / ↑ | Move to the next / previous option (selection updates with movement) | Required |
| Home / End | Move to the first / last option | Recommended |
| Character keys | Move to the next option starting with the typed character (rapid typing matches multiple characters) | Recommended |
Required WAI-ARIA Roles and Properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Container | role="listbox" | Indicates a collection of selectable items. |
| Container | aria-labelledby (or aria-label) | Provides an accessible name for the listbox. |
| Container | tabindex="0" + aria-activedescendant | Places focus on the list and points to the current option id (activedescendant pattern). |
| Each option | role="option" + unique id | A single option. The id is referenced by aria-activedescendant. |
| Each option | aria-selected="true | false" | Selection state. Must be updated whenever the selection changes. |
Implementation: Recommended Pattern (Good)
Good / Recommended
Place role="option" elements inside a role="listbox", and keep aria-selected and aria-activedescendant in sync with keyboard interaction.
Markup:
<span id="lb-label">Choose your favorite fruit</span>
<ul role="listbox"
id="lb-list"
tabindex="0"
aria-labelledby="lb-label"
aria-activedescendant="lb-opt-0">
<li role="option" id="lb-opt-0" aria-selected="true">Apple</li>
<li role="option" id="lb-opt-1" aria-selected="false">Orange</li>
<li role="option" id="lb-opt-2" aria-selected="false">Grape</li>
</ul>Keyboard interaction and selection sync:
const list = document.getElementById('lb-list');
const options = [...list.querySelectorAll('[role="option"]')];
const select = (index) => {
options.forEach((opt, i) => {
opt.setAttribute('aria-selected', String(i === index));
});
const active = options[index];
list.setAttribute('aria-activedescendant', active.id); // Communicates which option is active
active.scrollIntoView({ block: 'nearest' });
};
const current = () =>
options.findIndex((o) => o.getAttribute('aria-selected') === 'true');
list.addEventListener('keydown', (e) => {
const i = current();
let next = null;
if (e.key === 'ArrowDown') next = Math.min(i + 1, options.length - 1);
if (e.key === 'ArrowUp') next = Math.max(i - 1, 0);
if (e.key === 'Home') next = 0;
if (e.key === 'End') next = options.length - 1;
if (next !== null) { e.preventDefault(); select(next); }
});
// Also select on click
options.forEach((opt, i) => opt.addEventListener('click', () => {
select(i);
list.focus();
}));Anti-pattern (Bad)
Below is a list of <div> elements with only onclick handlers.You can click to select, but the list can't receive focus, arrow keys don't work, and the selection state is not communicated.
Try it: Tab cannot focus the list, and arrow keys do nothing. Screen readers don't recognize it as a selectable list, and the selection state is not announced.
<!-- ❌ Anti-pattern: div list + onclick -->
<div class="list">
<!-- No role, no aria-selected, completely inoperable via keyboard -->
<div class="option" onclick="pick(this)">Apple</div>
<div class="option" onclick="pick(this)">Orange</div>
<div class="option" onclick="pick(this)">Grape</div>
</div>Bad / Avoid
Problems with this implementation:
- Not keyboard operable —
divelements cannot receive focus, and arrow keys have no effect. - Role is not communicated — Without
role="listbox"/role="option", it is not recognized as a selectable list. - Selection state is not communicated — Without
aria-selected, there is no way to know which item is selected. - No label or position info — The group name and "X of Y" position are not announced.
Implementation Checklist
- The container has role="listbox" and a name (aria-labelledby, etc.)
- Each option has role="option" and a unique id
- aria-selected always matches the current selection state
- Focus management is implemented (aria-activedescendant or roving tabindex)
- Up / Down arrow keys move between options, and Home / End are available (recommended)
- Type-ahead moves to the matching option (recommended; especially important for lists with more than 7 items)
- Options can be selected by click, and focus is visible
Source (English):Listbox Pattern — W3C APG(opens in a new tab)