Data Display
Grid
AvailableAn interactive table (data grid) where individual cells are keyboard-navigable.
Specification updated
- Editorial#3356(opens in a new tab)Grid: Fix arrow key behavior description
What Is a Grid (Data Grid)?
A grid is an interactive UI that lets users navigate tabular data freely using arrow keys. Like a spreadsheet, you can move focus from cell to cell to interact with the data.
This kind of grid is different from the CSS layout grid. Also, if you just need to display data in a table, use a plain <table> — there's no need for a grid. Only use a grid when interaction is required, such as when you need cell-level focus movement or cells contain interactive elements.
Why Does Accessibility Matter?
The more interactive a table becomes, the more people get locked out when it's built incorrectly.
- Keyboard-only users: If cells only have
onclick, they can't be reached by Tab or arrow keys — making the table completely inoperable. - Screen reader users: Without
role="grid", the table isn't recognized as a grid — the row/column structure and current cell position aren't communicated.
The solution is to declare the structure with role="grid" and move focus with arrow keys (roving tabindex). The entire grid becomes a single tab stop, and navigation within it is handled by arrow keys.
Live Demo (Recommended Implementation)
The employee directory below is an interactive grid built following the APG.Without using a mouse, press Tab to enter the grid, then use arrow keys to move between cells.
Employee Directory
Try it: Tab to enter the grid → ↑ ↓ ← → to move between cells → Home / End for the first/last cell in a row, Ctrl + Home / Ctrl + End for the first/last cell in the grid.
Tip
Turn on a screen reader (on macOS, press ⌘+F5 for VoiceOver) and enter a cell. You'll hear something like "grid, 4 columns, 5 rows" describing the structure, and as you move, "Name column, Hanako Sato" announcingthe column header and cell value. This is the effect of role="grid" and column headers.
Keyboard Interaction
| Key | Action | Priority |
|---|---|---|
| Tab | Enter / exit the grid (the grid is a single tab stop) | Required |
| → / ← | Move focus to the right / left cell | Required |
| ↓ / ↑ | Move focus to the cell below / above | Required |
| Home / End | Move to the first / last cell in the current row | Required |
| Ctrl + Home / Ctrl + End | Move to the first / last cell in the grid | Recommended |
Note
Do not use Tab for cell navigation within the grid.Tab should be reserved for leaving the grid, while cell navigation is handled by arrow keys. This is the key difference from a native table.
Required WAI-ARIA Roles and Properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Outer wrapper | role="grid" | Declares the element as an interactive grid. |
| Outer wrapper | aria-label / aria-labelledby | Provides an accessible name for the grid (describes what the table represents). |
| Each row | role="row" | Identifies a row grouping. |
| Header cells | role="columnheader" | Column header. Read in association with cells in the same column. |
| Data cells | role="gridcell" | An individual cell. |
| Focusable cell | tabindex="0" (one only) / "-1" (all others) | Roving tabindex. Makes the grid a single tab stop; swap tabindex values as focus moves. |
Implementation: Recommended Pattern (Good)
Good / Recommended
Declare the structure with role="grid", and keep only one cell at tabindex="0"at a time, moving focus with arrow keys (roving tabindex).
Markup:
<div role="grid" aria-labelledby="grid-cap">
<div role="row">
<span role="columnheader" tabindex="-1">Employee ID</span>
<span role="columnheader" tabindex="-1">Name</span>
<span role="columnheader" tabindex="-1">Department</span>
<span role="columnheader" tabindex="-1">Extension</span>
</div>
<div role="row">
<!-- Only the first cell in the entire grid has tabindex="0" (the rest are -1) -->
<span role="gridcell" tabindex="0">E-001</span>
<span role="gridcell" tabindex="-1">Hanako Sato</span>
<span role="gridcell" tabindex="-1">Sales</span>
<span role="gridcell" tabindex="-1">1201</span>
</div>
<!-- Subsequent rows also use role="row" + role="gridcell" tabindex="-1" -->
</div>Keyboard interaction script (focus movement and tabindex management):
const grid = document.getElementById('grid-good');
if (grid) {
// Represent cells as a 2D array (array of cells per row)
const rows = Array.from(grid.querySelectorAll<HTMLElement>('[role="row"]'));
const cells = rows.map((row) =>
Array.from(row.querySelectorAll<HTMLElement>('[role="columnheader"],[role="gridcell"]'))
);
let r = 0; // Current row
let c = 0; // Current column
// Set tabindex="0" only on the current cell, -1 on all others (single tab stop)
function focusCell(nextR: number, nextC: number) {
const maxR = cells.length - 1;
r = Math.max(0, Math.min(nextR, maxR));
const maxC = cells[r].length - 1;
c = Math.max(0, Math.min(nextC, maxC));
cells.flat().forEach((cell) => cell.setAttribute('tabindex', '-1'));
const target = cells[r][c];
target.setAttribute('tabindex', '0');
target.focus();
}
grid.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowRight': focusCell(r, c + 1); break;
case 'ArrowLeft': focusCell(r, c - 1); break;
case 'ArrowDown': focusCell(r + 1, c); break;
case 'ArrowUp': focusCell(r - 1, c); break;
case 'Home':
if (e.ctrlKey) focusCell(0, 0); // First cell
else focusCell(r, 0); // First cell in row
break;
case 'End':
if (e.ctrlKey) focusCell(cells.length - 1, // Last cell
cells[cells.length - 1].length - 1);
else focusCell(r, cells[r].length - 1); // Last cell in row
break;
default: return; // Let the browser handle other keys
}
e.preventDefault();
});
// Sync current position when a cell is clicked
cells.forEach((row, ri) =>
row.forEach((cell, ci) => {
cell.addEventListener('click', () => focusCell(ri, ci));
})
);
}Note
If cells contain buttons or links, first ensure arrow key navigation between cells works, then consider adding an "edit mode" entered via Enter or F2 to interact with elements inside the cell. Start by making the one cell = one focus target basic pattern work reliably.
Anti-pattern (Bad)
Below is a table where cells only have onclick — it looks interactive but isn't.You can click cells with a mouse, but keyboard interaction is completely impossible.Try Tab → arrow keys and compare the experience to the demo above.
| Employee ID | Name | Department | Extension |
|---|---|---|---|
| E-001 | Hanako Sato | Sales | 1201 |
| E-002 | Taro Suzuki | Engineering | 1310 |
| E-003 | Misaki Takahashi | Human Resources | 1105 |
| E-004 | Ken Tanaka | Engineering | 1322 |
Try it: Tab doesn't focus any cell, and arrow keys don't work. A screen reader won't recognize it as a grid, and row/column/position information isn't communicated.
<!-- ❌ Anti-pattern -->
<!-- Looks like a table but doesn't function as an interactive grid -->
<table>
<tr>
<th>Employee ID</th><th>Name</th><th>Department</th><th>Extension</th>
</tr>
<tr>
<!-- onclick only. No role, tabindex, or key handling -->
<td onclick="select(this)">E-001</td>
<td onclick="select(this)">Hanako Sato</td>
<td onclick="select(this)">Sales</td>
<td onclick="select(this)">1201</td>
</tr>
</table>Bad / Avoid
Problems with this implementation:
- No keyboard access — Cells have no
tabindexor key handling, so arrow keys don't work. - Not recognized as a grid — Without
role="grid", assistive technology treats it as a plain table. - Not a single tab stop — Roving tabindex pattern is not implemented.
- Position not communicated — Current row, column, and selection state are not announced.
Tip
If your table doesn't need click or keyboard interaction, don't force it into a grid. A native <table> with <th scope> is perfectly fine. Only introduce role="grid" and roving tabindex when cell-level interaction is truly needed.
Implementation Checklist
- The outer wrapper has role="grid" and a name (aria-label / aria-labelledby)
- Rows use role="row", headers use role="columnheader", and cells use role="gridcell"
- Exactly one cell has tabindex="0" and all others have "-1" (roving tabindex)
- Arrow keys move between cells, and Home / End move to the edges of the row
- (Recommended) Ctrl+Home / End moves to the edges of the grid
- Tab exits the grid rather than moving between cells
- All operations are keyboard-accessible and focus is visible (focus ring is not hidden)
Source (English):Grid Pattern — W3C APG(opens in a new tab)