Forms & Controls
Listbox (single & multi-select)
A list of options a user can select one or more values from.
Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.
Implementation
Sort by
- Relevance
- Newest first
- Oldest first
- Price: low to high
- Price: high to low
Languages (multi-select)
- JavaScript
- TypeScript
- Python
- Rust
- Go
- Swift
- Kotlin
2 of 7 selected
listbox-pattern.tsx
function ListboxPattern() {
const [selected, setSelected] = useState(0);
const optionRefs = useRef([]);
function moveTo(index) {
const clamped = Math.max(0, Math.min(index, OPTIONS.length - 1));
setSelected(clamped); // single-select: selection follows focus
optionRefs.current[clamped]?.focus();
}
function onKeyDown(e) {
if (e.key === "ArrowDown") { e.preventDefault(); moveTo(selected + 1); }
else if (e.key === "ArrowUp") { e.preventDefault(); moveTo(selected - 1); }
else if (e.key === "Home") { e.preventDefault(); moveTo(0); }
else if (e.key === "End") { e.preventDefault(); moveTo(OPTIONS.length - 1); }
}
return (
<ul role="listbox" aria-labelledby="sort-label" tabIndex={-1} onKeyDown={onKeyDown}>
{OPTIONS.map((option, i) => (
<li
key={option}
role="option"
aria-selected={i === selected}
tabIndex={i === selected ? 0 : -1} // roving tabindex
onClick={() => moveTo(i)}
>
{option}
</li>
))}
</ul>
);
}Live demo
Sort by
- Relevance
- Newest first
- Oldest first
- Price: low to high
- Price: high to low
Languages (multi-select)
- JavaScript
- TypeScript
- Python
- Rust
- Go
- Swift
- Kotlin
2 of 7 selected
Required roles, states & properties
| Element | Attribute | Why |
|---|---|---|
| List container | role="listbox" | Identifies the element as a listbox so AT announces it as a selectable list, not a generic group of text. |
| List container | aria-label / aria-labelledby | Gives the listbox an accessible name so it's announced as e.g. "Sort by, listbox" rather than an unnamed list. |
| List container (multi-select only) | aria-multiselectable="true" | Tells AT more than one option may be selected at once, changing how selection state is announced. |
| Each option | role="option" | Identifies each row as a selectable option within the listbox. |
| Each option | aria-selected | Communicates each option's individual selected/not-selected state — required on every option, including unselected ones. |
| Each option | tabIndex (roving) | Only the active option has tabIndex=0 so Tab moves past the whole widget in one stop; arrow keys move the roving cursor among options with tabIndex=-1. |
Keyboard interaction model
| Key | Behavior |
|---|---|
| Down Arrow | Single-select: moves focus to the next option AND selects it. Multi-select: moves focus only, selection unchanged. |
| Up Arrow | Single-select: moves focus to the previous option AND selects it. Multi-select: moves focus only. |
| Space | Multi-select only: toggles the focused option's selected state. |
| Shift + Down/Up | Multi-select only: extends a contiguous selection range from the last toggled option to the newly focused option. |
| Ctrl/Cmd + A | Multi-select only: selects all options (simplified in this demo's key handler; document the full toggle-all behavior in production). |
| Home / End | Moves focus (and, in single-select, selection) to the first / last option. |
Focus management rules
- Only one option is in the Tab order at a time (roving tabindex); Tab moves straight past the whole listbox.
- Single-select: moving the roving-tabindex cursor changes selection immediately (selection follows focus).
- Multi-select: moving the roving-tabindex cursor never changes selection by itself — only Space or Shift+Arrow does.
- Clicking an option moves both DOM focus and the roving-tabindex cursor to that option.
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. |
| 4.1.2 | Name, Role, Value | A | For all UI components, name, role, and value are programmatically determinable; states and changes are announced. |