Choosing Between grid and table Roles
Permalink to "Choosing Between grid and table Roles"role="table" (the implicit role of <table>) describes static tabular data that users read. role="grid" describes an interactive widget whose cells users operate, moving between them with arrow keys. The two look identical on screen, which is why grids are over-used: a team adds sorting or a row menu, decides the table is “interactive now”, and switches to role="grid" — taking on a whole keyboard model they then only half-implement, and changing how every screen reader reads the data.
This page is a decision guide. It belongs to composite widget roles & states.
Spec reference
Permalink to "Spec reference"table (ARIA 1.2): “a section containing data arranged in rows and columns … not intended for interaction.” Screen readers read it in browse mode with table navigation commands (Ctrl+Alt+Arrow in NVDA and JAWS, VO+Arrow in VoiceOver). Interactive elements inside cells — links, buttons, checkboxes — are fine and are reached with Tab as usual.
grid (ARIA 1.2): “a composite widget containing a collection of one or more rows with one or more cells where some or all cells in the grid are focusable by using methods of two-dimensional navigation, such as directional arrow keys.” A grid is one tab stop; arrow keys move between cells. Screen readers switch to focus or forms mode on entering it, so their own table-reading commands stop working and your arrow-key handling takes over.
That last point is the crux. Choosing grid hands keyboard navigation from the screen reader to your code. If your code only moves focus between a few interactive cells, users lose the ability to read the rest.
Criteria: SC 1.3.1 Info and Relationships (both roles convey structure), SC 2.1.1 Keyboard, SC 4.1.2 Name, Role, Value (the role must match the behaviour).
When to use each
Permalink to "When to use each"Native table when users read data and act on rows: sortable headers, a link per row, a checkbox column, a row actions menu button. All of those work with Tab inside a table, and users keep full browse-mode reading. This covers the large majority of data tables in business applications.
Grid when users work in cells: spreadsheet-style editing, cell-range selection, calendars and schedulers, a data-entry sheet. Arrow-key movement is genuinely faster there, and the grid’s single tab stop saves users from tabbing through hundreds of inputs.
Treegrid when a grid’s rows nest and expand. Listbox when users select records from a list with few columns — see listbox versus grid for selectable record lists.
The misapplication to name is role="grid" on a read-only report because a component library’s “DataGrid” component sets it by default. Users lose browse-mode reading and gain arrow keys that move between cells they cannot do anything with.
Annotated code example
Permalink to "Annotated code example"<!-- TABLE: read + row actions. Tab reaches the buttons; browse mode reads everything -->
<table>
<caption>Invoices</caption>
<thead><tr>
<th scope="col"><button type="button">Invoice</button></th> <!-- sortable -->
<th scope="col">Amount</th>
<th scope="col"><span class="visually-hidden">Actions</span></th>
</tr></thead>
<tbody><tr>
<th scope="row"><a href="/inv/1042">INV-1042</a></th>
<td>1,280.00</td>
<td><button type="button" aria-haspopup="menu">Actions for INV-1042</button></td>
</tr></tbody>
</table>
<!-- GRID: cells are the unit of interaction; one tab stop, arrows move -->
<div role="grid" aria-label="Budget 2026" aria-rowcount="13" aria-colcount="5">
<div role="row" aria-rowindex="1">
<div role="columnheader">Category</div>
<div role="columnheader">Q1</div>
</div>
<div role="row" aria-rowindex="2">
<div role="rowheader">Travel</div>
<!-- SC 2.1.1: one cell holds tabindex=0; the rest -1 (roving tabindex) -->
<div role="gridcell" tabindex="0">4,000</div>
</div>
</div>
A grid can be built on a native <table> with role="grid" on the table element; the row and cell roles are then mapped to row and gridcell automatically. That keeps the HTML semantic while changing the interaction model — and it is still a commitment to implement the full grid keyboard model.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Aspect | Native table | role=“grid” |
|---|---|---|
| Screen reader mode on entry | Browse mode | Focus / forms mode (automatic in most readers) |
| Moving between cells | Reader’s table commands | Your arrow-key handler |
| Tab | Moves through links and buttons in cells | Moves past the whole grid (one tab stop) |
| Reading non-interactive cells | Always possible | Only if your arrows land on every cell |
| Headers announced | By the reader, from th/scope |
By the reader, from columnheader/rowheader on focus |
| Cost to implement | Markup only | Roving tabindex, key handling, edit modes |
Integration context
Permalink to "Integration context"Once a grid is the right choice, its keyboard contract is implemented with roving tabindex or aria-activedescendant, plus the edit-mode rules in entering and exiting cell edit mode.
The screen reader mode switch is the most visible consequence of the role and the source of many “my arrow keys don’t work” bug reports; JAWS virtual cursor versus forms mode in data grids explains the JAWS side.
Gotchas
Permalink to "Gotchas"Library defaults. Several data-grid libraries render role="grid" regardless of whether anything is interactive. Check for a “table mode” or override the role for read-only uses.
Half-built grids. role="grid" with arrow keys that only visit interactive cells leaves static cells unreadable. Every cell must be reachable.
Grids inside tables, tables inside grids. Nesting composite widgets confuses every reader. Flatten the design.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"When should I use role="grid" instead of a table?
When users interact with cells themselves and moving between cells with arrow keys helps — spreadsheet editing, cell selection, schedulers. For data that is read and acted on per row, keep a native table.
Does adding sorting make a table a grid?
No. Sort buttons in header cells work perfectly inside a native table and are reached with Tab. The table stays a table, and screen reader users keep their table-reading commands.
Why do screen reader table commands stop working in my grid?
Because role=“grid” puts screen readers into focus or forms mode, handing arrow keys to your code. That is the intended behaviour for grids; if users mostly read the data, the component should be a table.
Can I use role="grid" on a native table element?
Yes. Rows and cells are then exposed as row and gridcell. It keeps the HTML semantic, but you still have to implement the full grid keyboard model.
Related
Permalink to "Related"- Listbox versus grid — the other common alternative
- Roving tabindex for grids — what the grid role commits you to
- JAWS virtual cursor versus forms mode — how the role changes screen reader mode