Listbox Versus Grid for Selectable Record Lists
Permalink to "Listbox Versus Grid for Selectable Record Lists"Many data interfaces have a list of records on the left and a detail pane on the right: an inbox, a queue of tickets, a list of customers. Each record shows a few fields — sender, subject, date — and users pick one to open. That list can be built as role="listbox" (each record is an option) or as role="grid" (each record is a row of cells). Both are correct for some lists and wrong for others, and the choice changes what users hear on every arrow press.
This page compares them and gives a rule for choosing. It belongs to composite widget roles & states.
Spec reference
Permalink to "Spec reference"listbox contains option elements. One tab stop; Up/Down Arrow move between options; aria-selected marks selection; aria-multiselectable allows several. Each option is read as a single string — its accessible name — plus “selected” and position (“3 of 40”). Options may not contain interactive descendants.
grid contains row elements containing gridcell elements. One tab stop; arrows move in two dimensions; rows or cells can be selectable. Each move reads the focused cell (or row, with row focus) with its column header.
Criteria: SC 4.1.2 Name, Role, Value — the option’s name is the only thing a listbox user hears, so it must summarise the record; SC 1.3.1 — in a grid, the column relationship is programmatic; SC 2.1.1 Keyboard.
When to use each
Permalink to "When to use each"Listbox when a record is something you pick: its fields are a description, users scan by hearing the whole thing, and the only action is “select this one”. Master–detail lists, pickers in dialogs, recipient suggestions. Keep the visible fields to three or four so the spoken name stays short.
Grid when users need to compare fields across records or act on individual fields: jumping down the Date column to find a range, toggling a star in one column, a per-row checkbox plus a per-row menu. Columns need headers, and the grid needs the full two-dimensional keyboard model.
Neither when the list is really a table that users read: use a native table with a link per row. Selection is then a checkbox column, and opening a record is following its link.
The misapplication to name is a listbox whose options contain buttons (star, archive, menu). ARIA forbids interactive descendants in options, screen readers cannot reach them, and keyboard users cannot activate them without a mouse.
Annotated code example
Permalink to "Annotated code example"<!-- LISTBOX: each option is named by a summary of the record -->
<div role="listbox" aria-label="Inbox" tabindex="0"
aria-activedescendant="msg-2"> <!-- focus stays on the listbox -->
<div role="option" id="msg-1" aria-selected="false">
<!-- SC 4.1.2: visible fields make up the accessible name -->
<span class="from">Contoso</span>
<span class="subject">Weekly report</span>
<time datetime="2026-03-03">3 March</time>
</div>
<div role="option" id="msg-2" aria-selected="true">
<span class="from">Northwind</span>
<span class="subject">Invoice overdue</span>
<time datetime="2026-03-04">4 March</time>
</div>
</div>
<!-- GRID: fields as cells with column headers, actions allowed in cells -->
<div role="grid" aria-label="Inbox" aria-multiselectable="true">
<div role="row">
<div role="columnheader">From</div><div role="columnheader">Subject</div>
<div role="columnheader">Date</div><div role="columnheader">Star</div>
</div>
<div role="row" aria-selected="true">
<div role="gridcell" tabindex="0">Northwind</div>
<div role="gridcell" tabindex="-1">Invoice overdue</div>
<div role="gridcell" tabindex="-1">4 March</div>
<div role="gridcell"><button tabindex="-1" aria-pressed="false">Star</button></div>
</div>
</div>
// Listbox: single-select, selection follows focus (common for master–detail)
listbox.addEventListener('keydown', (e) => {
const opts = [...listbox.querySelectorAll('[role="option"]')];
let i = opts.findIndex((o) => o.id === listbox.getAttribute('aria-activedescendant'));
if (e.key === 'ArrowDown') i = Math.min(i + 1, opts.length - 1);
else if (e.key === 'ArrowUp') i = Math.max(i - 1, 0);
else if (e.key === 'Home') i = 0;
else if (e.key === 'End') i = opts.length - 1;
else return;
e.preventDefault();
opts.forEach((o) => o.setAttribute('aria-selected', 'false'));
opts[i].setAttribute('aria-selected', 'true');
listbox.setAttribute('aria-activedescendant', opts[i].id);
opts[i].scrollIntoView({ block: 'nearest' });
showDetail(opts[i].id); // detail pane updates, focus stays
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Listbox | Grid |
|---|---|---|
Down Arrow |
Next record, read in full: “Northwind, Invoice overdue, 4 March, 2 of 40” | Same column, next row: “Contoso” (header announced on column change only) |
Right Arrow |
Nothing (or next option in horizontal listboxes) | Next field: “Subject, Weekly report” |
Space |
Toggle selection (multi-select) | Toggle row selection |
Enter |
Open the record | Activate the cell’s control or open the record |
| Type-ahead | Jumps to option starting with typed text (if implemented) | Rarely implemented |
Integration context
Permalink to "Integration context"Both roles can manage focus with aria-activedescendant (focus stays on the container) or roving tabindex (focus moves to each option or cell); the trade-offs are in using aria-activedescendant for grid cell focus.
For multi-select grids, the selection model and its announcements are in aria-selected on cells versus rows. The broader table-versus-grid question is in choosing between grid and table roles.
Gotchas
Permalink to "Gotchas"Long option names. Seven visible fields make a 25-word option name read on every arrow press. Trim the name with aria-label to the fields that identify the record, or switch to a grid.
Selection following focus in multi-select listboxes. Only follow focus in single-select lists; in multi-select, arrows must move without changing selection.
Virtualised listboxes. Options not rendered cannot be aria-activedescendant targets. Set aria-setsize and aria-posinset on rendered options so position is honest — see aria-setsize and aria-posinset in virtual lists.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"Should an inbox-style list be a listbox or a grid?
A listbox when users pick one message and hear it as a single phrase; a grid when they need to move between fields such as sender and date, or act on individual controls like a star in each row.
Can listbox options contain buttons?
No. ARIA does not allow interactive descendants in options, and screen readers cannot reach them. If each record needs its own controls, use a grid, or a table with a link and buttons per row.
Should selection follow focus in a listbox?
In a single-select listbox used for master–detail, it usually should, so the detail pane follows the user. In a multi-select listbox it must not, or moving would clear the selection.
How long can a listbox option's name be?
Keep it short — the identifying fields, ideally under a dozen words — because it is read in full on every arrow press. If users need more fields, a grid lets them read one at a time.
Related
Permalink to "Related"- Choosing between grid and table roles — the other role decision
- aria-selected on cells versus rows — row selection in a grid
- aria-activedescendant for focus — a focus technique for both roles