Data Grid
UI pattern
A data grid is an interactive table. It presents data in rows and columns, and adds keyboard navigation: users move focus from cell to cell with the arrow keys, then operate the links, buttons, checkboxes, or text fields a cell holds.
A data grid is a composite widget, so the whole grid is a single stop in the page's tab order, however many controls it holds. Once focus is inside, the grid's script moves it between cells, as described in Managing Focus. A table is different: it has no keyboard interaction of its own, and every control in it is a separate tab stop.
A data grid is built from the same parts as a table. The grid holds rows, directly or inside row groups, and each row holds grid cells. A grid cell is a data cell that users can focus and operate.
In a data grid, a cell's row and column say something about what it holds, so the grid also has header cells that tell users how each cell relates to its row and column.
The grid role describes how the cells relate, not how they look. A grid can also hold a set of controls whose positions mean little or nothing, such as a list of message recipients that wraps onto as many rows as the screen width allows. This is a layout grid.
A layout grid is built and operated like a data grid, and nearly everything in this entry applies to it. It usually has no header cells, since there are no row and column relationships for headers to explain, and its keyboard navigation differs in a few ways, as Keyboard Operation describes.
When to Use
Use a data grid when:
- Users need to move through tabular data cell by cell with the arrow keys, such as in a spreadsheet, a scheduling grid, or a data management table.
- Many cells hold interactive controls, such as buttons, checkboxes, links, or editable fields. With one tab stop for the whole grid, keyboard users can move past it without tabbing through every control.
- Users need to select rows or cells from the keyboard.
- A long set of controls, such as product cards or message recipients, would otherwise put a stop in the tab order for each one. A layout grid makes the set one tab stop that users move through with the arrow keys.
Do not use a data grid when:
- The data is read-only, or only a few cells hold controls. Use a table instead.
- The only interaction is a clickable row. Put a link or button in the row of a table rather than turning the table into a grid.
- Rows and columns are only a way to position content on screen, and users don't need to move through it with the arrow keys. Use CSS grid or flexbox, which arrange content without changing its role, and keep
role="grid"for a widget.
Examples
Code in this section is meant to demonstrate techniques rather than requirements and lacks features needed in a production application.
A data grid needs more than interactive elements in a table. It needs role="grid" on its container, and code that manages focus inside it with either the roving tabindex technique or the active descendant technique, as described in Developing a Keyboard Interface.
Data Grid Using the HTML Table Element
This example sets up the structure of a Tic-Tac-Toe board as a data grid built on native table elements. It leaves out the script that handles keyboard navigation and plays the game. Building on a native table gives the grid the browser's header association, cell spanning, and accessible name from the <caption>.
Native data grid HTML
<table role="grid">
<caption>Play Tic-Tac-Toe</caption>
<thead>
<tr>
<th
scope="col"
aria-label="Row">
</th>
<th scope="col">Column 1</th>
<th scope="col">Column 2</th>
<th scope="col">Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Row 1</th>
<td>
<button type="button" tabindex="0">
X
</button>
</td>
<td>
<button
type="button"
tabindex="-1"
aria-label="Empty">
</button>
</td>
<td>
<button type="button" tabindex="-1">
O
</button>
</td>
</tr>
<!-- Rows 2 and 3 follow the same pattern -->
</tbody>
</table>Setting role="grid" on the <table> changes the implicit roles of its cells, so the table element is usually the only place you set a role:
| Element | Default role | Role inside <table role="grid"> |
|---|---|---|
<table> | table | grid |
<caption> | caption | caption |
<thead>, <tbody>, <tfoot> | rowgroup | rowgroup |
<tr> | row | row |
<th scope="col"> | columnheader | columnheader |
<th scope="row"> | rowheader | rowheader |
<td> | cell | gridcell |
The example uses a roving tabindex. Only the active cell's button has tabindex="0", and every other button has tabindex="-1", so the grid is one stop in the tab order. Each cell holds one button, so arrow-key navigation moves focus to the button rather than to the cell.
Data Grid Using ARIA
The grid role doesn't imply a visual layout. When two-dimensional data needs a presentation other than a table, you can build the grid from generic elements with ARIA roles, as this version of the same board does. The grid takes its accessible name from the heading through aria-labelledby, and each data cell carries role="gridcell".
ARIA data grid HTML
<h2 id="game-label">Play Tic-Tac-Toe</h2>
<div
role="grid"
aria-labelledby="game-label">
<div role="rowgroup">
<div role="row">
<span
role="columnheader"
aria-label="Row"></span>
<span role="columnheader">
Column 1
</span>
<span role="columnheader">
Column 2
</span>
<span role="columnheader">
Column 3
</span>
</div>
</div>
<div role="rowgroup">
<div role="row">
<span role="rowheader">
Row 1
</span>
<span role="gridcell">
<button type="button" tabindex="0">
X
</button>
</span>
<span role="gridcell">
<button
type="button"
tabindex="-1"
aria-label="Empty">
</button>
</span>
<span role="gridcell">
<button type="button" tabindex="-1">
O
</button>
</span>
</div>
<!-- Rows 2 and 3 follow the same pattern -->
</div>
</div>For how the row group, row, and header roles map to native table elements, refer to Table › Custom ARIA Table.
Structure and Semantics
A data grid's container carries the grid role and an accessible name, so assistive technologies identify it as an interactive widget rather than a static table, and screen reader users are told they can move through it with the arrow keys. The name comes from a <caption> on a native table, or from aria-labelledby pointing at a visible heading.
Inside the grid, rows carry row, directly under the grid or inside a rowgroup, and cells carry gridcell, columnheader, or rowheader. A native <table role="grid"> supplies the row and cell roles itself.
A data grid adds ARIA states and properties for the features it supports:
| Attribute | How to use |
|---|---|
aria-multiselectable | Set to true on the grid when users can select more than one row or cell. |
aria-selected | Set on each selectable row or cell to indicate whether it's selected. When the grid is multiselectable, give every selectable row or cell an explicit value. |
aria-readonly | Set on a cell, or on the grid, only when the grid supports editing and editing is turned off there. |
aria-sort | Set on the header of the column or row the grid is sorted by. |
aria-disabled | Set on a row or cell that's unavailable, and stop its actions in code. |
aria-expanded | Set on a row that users can expand to reveal detail rows. |
aria-rowcount, aria-colcount | Set on the grid when the DOM holds only part of the data, paired with aria-rowindex and aria-colindex on rows or cells. |
Grid Has Cells
A data grid holds rows, and each row holds the cells users move between. Without them, there's nothing for arrow-key navigation to reach.
Gives members the required role
Reported as: No group members with required role
A data grid must contain cells that users can move between with the arrow keys.
When the grid's children are generic elements, assistive technologies can't convey its rows, columns, or cell positions, and the grid's navigation has nothing to reach. Screen reader users then hear a grid with nothing in it, or content with no sense of where it sits.
Build the grid on a native <table>, or build it from elements with the row and cell roles. Remove any wrapper elements between the grid and its rows, other than row groups. Table Has Rows, Row Group Has Rows, and Row Has Cells cover each level of the structure.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Table Structure
A data grid built on a native <table> keeps a table's header and footer sections. HTML allows one <thead> and one <tfoot> in a table, so browsers and screen readers expect a single header section and a single footer section.
Groups footer rows in one element
Reported as: Table has multiple footers
A data grid must place all its footer rows in a single <tfoot> element.
When a grid has more than one <tfoot>, browsers don't handle the extras consistently, and the reading order can change. Screen reader users may hear totals or summary rows in the wrong place, or be unable to tell them from ordinary data.
Move every footer row into one <tfoot>.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Groups header rows in one element
Reported as: Table has multiple headers
A data grid should place all its header rows in a single <thead> element.
When a grid has more than one <thead>, browsers treat the extras as body sections. Screen reader users may then hear header rows as data, or hear a header twice.
Move every header row into one <thead>. A single <thead> can hold several header rows.
Severity: Minor
Table Has Rows
A data grid holds its rows directly or inside row groups, and assistive technologies report the grid's size from the rows they find.
Gives members the required role
Reported as: No group members with required role
A data grid must contain at least one row, directly or inside a row group.
When a grid has no descendants with the row role, screen reader users hear that a grid is there but find nothing to move through, and they can't tell whether the data failed to load or doesn't exist.
Render the grid only when it has rows, and show a message outside it when there's no data. In a grid built from generic elements, give each row role="row".
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Row Group Has Rows
A row group exists to hold rows, and assistive technologies present it as a section of the grid, such as its header rows or its body.
Gives members the required role
Reported as: No group members with required role
A data grid's row group must contain at least one element with the row role.
When a <thead>, <tbody>, <tfoot>, or role="rowgroup" element has no rows, screen reader users moving by section reach a group with nothing in it and can't tell whether content failed to load.
Render a row group only when it has rows, and make sure each row inside it is a <tr> or carries role="row".
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Row Has Cells
Each row holds the cells for one line of the grid, and assistive technologies convey each cell's column as users move along the row.
Gives members the required role
Reported as: No group members with required role
A data grid's row must contain at least one cell with the gridcell, columnheader, or rowheader role.
When a row has no cells, or its children are generic elements, keyboard users moving down a column reach a row with nothing to focus, and its columns no longer line up with the rows around it.
Put a <td> or <th> in every row of a grid built on a native table. In a grid built from generic elements, give each cell the gridcell, columnheader, or rowheader role.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Unambiguous Headers
In a data grid, every data cell can be matched to the header cells that label its row and column, so screen readers can announce those headers as users move into the cell. In a native grid, scope on each <th> declares what it heads: col, row, colgroup, or rowgroup. A header that spans several columns or rows uses colspan or rowspan. In a grid built from generic elements, aria-colspan and aria-rowspan set the same spans, but they don't replace the header association a native table provides.
These criteria are about data grids. A layout grid's cells have little or no row and column relationship, so it usually has no header cells to associate.
Builds spanned headers natively
Reported as: Complex headers in ARIA table
A data grid with header cells that span rows or columns should be built on a native <table>.
When a grid built from generic elements sets aria-colspan or aria-rowspan on its headers, some screen readers don't associate the spanning header with the cells beneath it, even inside a row group. Users then hear data without the header that explains it.
Build the grid on <table role="grid">, with colspan or rowspan and scope on each spanning <th>, so the browser can apply its own header association. If the grid has to be built from generic elements, don't rely on aria-colspan and aria-rowspan alone. Point each cell's aria-describedby at the spanning header as well.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(g) | Row and column headers shall be identified for data tables. |
Matches header scope to its span
Reported as: Conflicting scope spanning values
A data grid's header cell that spans several columns or rows must declare that it heads the group.
When a <th> with colspan has scope="col", or one with rowspan has scope="row", it claims to head a single column or row. Screen reader users then hear it with the wrong cells, or miss it on some of the cells it labels.
Set scope="colgroup" on a header with colspan, and scope="rowgroup" on a header with rowspan.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(h) | Markup shall be used to associate data cells and header cells for data tables that have two or more logical levels of row or column headers. |
Has header cells
Reported as: Missing table headers
A data grid must have header cells that identify what each column or row holds. A layout grid, whose cells' positions carry little or no meaning, doesn't need them.
When a data grid has no <th> elements and no elements with role="columnheader" or role="rowheader", screen reader users moving between cells hear values with nothing to say what they mean.
Mark the cells that label columns or rows as header cells: <th> in a grid built on a native table, or role="columnheader" and role="rowheader" in one built from generic elements.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(g) | Row and column headers shall be identified for data tables. |
Points each header at its cells
Reported as: Table headers missing scope attribute
Each <th> in a data grid must declare with scope whether it labels a column or a row.
When scope is missing, browsers guess the association from the grid's layout, and different browsers can guess differently. Screen reader users may then hear the wrong header for a cell, or none.
Add scope="col" or scope="row" to every <th>, or scope="colgroup" or scope="rowgroup" to a spanning header.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(h) | Markup shall be used to associate data cells and header cells for data tables that have two or more logical levels of row or column headers. |
Role
A data grid's container carries the grid role. The role tells assistive technologies the structure is interactive, so screen readers switch to the mode in which the arrow keys go to the grid rather than to reading commands.
Its data cells carry the gridcell role, which marks each one as a cell users can focus and operate. A <td> inside <table role="grid"> gets the role without one of its own.
Has the correct role
Reported as: Element has incorrect role
A data grid's container must carry the grid role, and its data cells the gridcell role.
When the container carries table, another role, or none, screen reader users aren't prompted to use the arrow keys. Their screen reader may keep those keys for its own reading commands, so the grid's navigation never runs. A data cell with another role isn't exposed as a cell users can operate.
Set role="grid" on the container, whether it's a <table> or a generic element. In a grid built from generic elements, set role="gridcell" on each data cell.
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Applies role to the correct element
Reported as: Incorrect root element
Sanity check. Evinced checks that the grid role sits on the element that contains the grid's rows, rather than on one of its descendants. This check runs on every UI pattern, not only on data grids.
A data grid's grid role should sit on the outermost element that contains its rows.
When the role is set on a descendant rather than on the container, assistive technologies don't identify the container as a grid, and rows outside the element carrying the role fall outside the widget users navigate.
Move role="grid" to the element that contains every row of the grid. If the test selected an inner element by mistake, select the container and run the test again.
Accessible Name
Each grid cell takes its accessible name from its content, and a header cell's name is what screen readers announce as users move into the cells it labels. A cell whose only content is an icon, an image, or an empty control has no name unless you provide one, as the empty square's aria-label does in the examples.
Has an accessible name
Reported as: Accessible name
A data grid's cells must have an accessible name that conveys their content.
When a grid cell holds only an icon or image with no text alternative, or an unnamed control, screen reader users who move focus to it hear an empty cell, or a control they can't identify.
Give an image in the cell alt text, name a control in the cell with its text or an aria-label, or add visually hidden text to the cell. Say what an icon means, such as "Available" rather than "Check mark".
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Accessibility Label
A data grid has an accessible name that says what it holds, separate from the names of its cells. Screen readers announce the name when focus enters the grid, so it's how users tell one grid from another before reading any cells.
Names a container that needs one
Reported as: Missing contextual labeling
A data grid, native or built from generic elements, must have an accessible name that identifies what it holds.
When a grid has no name from any source, screen reader users hear only that they've entered a grid, with no clue which one or what it's for. On a page with several grids or tables, they all sound the same.
Give the grid a name with a <caption> on a native table, or with aria-labelledby pointing at the visible heading that introduces it, as the Examples do. Use aria-label only when there's no visible text to reference.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Carries a caption or a label
Reported as: Table missing accessible name
A data grid built on a native <table> should carry a <caption> that says what it holds, or an accessible name set with ARIA.
When only the prose or heading above the grid identifies it, users who reach it by jumping from table to table find a grid with no name and have to read its headers to learn what it holds.
Add a <caption> as the first child of the <table>. If the design has no room for visible text, keep the <caption> and hide it visually with a CSS utility class, or reference existing text with aria-labelledby.
Severity: Moderate
Fills the caption it declares
Reported as: Table has empty caption
A data grid's caption should hold text that identifies the grid, or not be there at all.
When the caption is empty or holds only whitespace, the grid gets an empty accessible name. Screen reader users get no indication of what the grid holds, even though the markup looks as if it names the grid.
Write a short phrase in the caption that says what the grid holds. If you don't want a caption, remove the element and name the grid with aria-labelledby or aria-label. In a template, render the caption only when there's text for it.
Severity: Moderate
Cell Index
Data grids often render only part of their data, such as one page of results or the rows in view of a virtual scroller. Such a grid declares its full size with aria-rowcount and aria-colcount, and each rendered row or cell declares its position with aria-rowindex and aria-colindex. A missing, invalid, or inconsistent value can make screen readers announce the wrong position as users move between cells, or skip rows and columns.
The following example shows a grid of 500 rows and 6 columns, scrolled so that row 41 is in view. The same attributes work on the grid, row, and cell elements of a grid built from generic elements.
<table role="grid" aria-rowcount="500" aria-colcount="6">
<caption>Orders</caption>
<thead>
<tr aria-rowindex="1">
<th scope="col" aria-colindex="1">Order</th>
<th scope="col" aria-colindex="2">Status</th>
</tr>
</thead>
<tbody>
<tr aria-rowindex="41">
<td aria-colindex="1">10041</td>
<td aria-colindex="2">Shipped</td>
</tr>
<!-- Only the rows in view are rendered -->
</tbody>
</table>Indexes the columns it counts
Reported as: Missing aria-colindex
When a data grid declares aria-colcount, each of its rows or each of its cells must declare a column position with aria-colindex.
When the positions are missing, assistive technologies know how many columns the grid has but not which of them are rendered, so screen reader users hear the wrong column position for each cell.
Add aria-colindex to every row when the rendered columns are consecutive, or to every cell when they're not. Use the same level throughout the grid.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(h) | Markup shall be used to associate data cells and header cells for data tables that have two or more logical levels of row or column headers. |
States the true column total
Reported as: Aria-colcount incorrect value
A data grid's aria-colcount must count every column in its data, including columns that aren't rendered.
When the value is lower than the number of cells already rendered in a row, the grid contradicts itself, and screen reader users are told the grid is narrower than what they can reach.
Set aria-colcount to the total number of columns. If the total isn't known, set it to -1.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(h) | Markup shall be used to associate data cells and header cells for data tables that have two or more logical levels of row or column headers. |
States the true row total
Reported as: Aria-rowcount incorrect value
A data grid's aria-rowcount must count every row in its data, including header rows and rows that aren't rendered.
When the value is lower than the number of rows already rendered, screen reader users are told the grid has fewer rows than they can reach, and the positions they hear stop making sense.
Set aria-rowcount to the total number of rows. If the total isn't known, set it to -1.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(h) | Markup shall be used to associate data cells and header cells for data tables that have two or more logical levels of row or column headers. |
Counts columns when indexing them
Reported as: Aria-colindex with no aria-colcount context
A data grid that uses aria-colindex should declare its total number of columns with aria-colcount.
When the grid has no aria-colcount, a column position has nothing to count against, so screen reader users hear a position without learning how wide the grid is.
Add aria-colcount to the grid element, set to the total number of columns, or -1 if the total isn't known.
Severity: Minor
Counts rows when indexing them
Reported as: Aria-rowindex with no aria-rowcount context
A data grid that uses aria-rowindex should declare its total number of rows with aria-rowcount.
When the grid has no aria-rowcount, a row position has nothing to count against, so screen reader users hear a position without learning how long the grid is.
Add aria-rowcount to the grid element, set to the total number of rows, or -1 if the total isn't known.
Severity: Minor
Indexes the whole set in order
Reported as: Inconsistent index values
A data grid's row and column indexes should be set on every rendered row or cell, with values that increase in order.
When some rows or cells carry an index and others don't, or a value repeats or goes backward, assistive technologies can't work out where a cell sits. Screen reader users then hear wrong positions as they move between cells, or find table-reading commands skipping rows.
Set the index on every rendered row, or on every cell, with each value greater than the one before. A jump in the numbers is fine when the rows or columns between them aren't rendered.
Severity: Minor
Keeps indexes within the total
Reported as: Index out of range
A data grid's aria-rowindex and aria-colindex values should not exceed the totals set in aria-rowcount and aria-colcount.
When an index is higher than its total, screen reader users hear an impossible position, such as row 60 of 50.
Update the count when the data grows, and derive each index from the same data that sets the count.
Severity: Minor
Indexes columns in one place
Reported as: Mixed aria-colindex implementation
A data grid must declare column positions with aria-colindex on its rows or on its cells, not on both.
When both levels carry aria-colindex, assistive technologies get two conflicting sources of column position, and screen reader users hear the wrong column for some cells.
Set aria-colindex on each row when the rendered columns are consecutive, or on each cell when they're not, and remove it from the other level.
Severity: Minor
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(h) | Markup shall be used to associate data cells and header cells for data tables that have two or more logical levels of row or column headers. |
Numbers positions from 1 up
Reported as: Negative row or column index
A data grid's aria-rowindex and aria-colindex values should be positive whole numbers, starting at 1.
When a value is zero or negative, assistive technologies ignore it or misreport it, and screen reader users hear a wrong position for the cell or none at all.
Number rows and columns from 1. If your data uses zero-based indexes, add 1 to each.
Severity: Minor
aria-readonly
aria-readonly indicates that a cell's content can't be edited, and it applies only to a grid that supports editing. A read-only cell is still focusable and reachable with the arrow keys, since users need to read it.
Limits aria-readonly to editing
Reported as: Avoid aria-readonly attribute
A data grid should set aria-readonly on cells, rows, or the grid itself only when the grid supports editing and editing is turned off there.
When a grid that offers no editing marks cells read-only, screen reader users hear a state that tells them nothing, or look for an edit function that doesn't exist. Screen readers also announce the state inconsistently, so users may miss it where it matters.
Leave aria-readonly out of a grid that has no editing functions. In an editable grid, set it on the cells where editing is off, or on the grid when it's off everywhere, and keep those cells focusable.
Severity: Minor
Disabled
Parts of a data grid can be unavailable, such as a row that can't be edited or a cell whose action is turned off. Unlike a static table, a data grid supports aria-disabled on the grid itself and on its rows, grid cells, column headers, and row headers. The attribute doesn't block interaction on its own, so a disabled row or cell still needs its actions stopped in code.
Disables only when role allows
Reported as: Component does not support disabled state
A data grid's parts must declare a disabled state only when their role supports one.
When a row group or a caption carries disabled or aria-disabled, assistive technologies ignore it or report a state users can't act on. Users may then think the rows are unavailable when nothing in them has changed.
Remove the attribute, and put aria-disabled on each row or grid cell that's unavailable instead, or on the grid when all of it is.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Disables only elements that support it
Reported as: Element type does not support disabled state
An element in a data grid must carry disabled or aria-disabled only when its role supports a disabled state.
When the state sits on an element whose role has none, such as a wrapper, a list item or a heading, assistive technology may drop it or report it on something users can't operate, while the control that really is unavailable is still reported as available.
Remove the attribute from that element, and set the disabled state on the control itself, using an element or role that supports it, such as a native <button>.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Avoids conflicting disabled states
Reported as: Conflicting disabled values
A data grid's parts, and the controls in its cells, must each have a single, consistent disabled state.
When an element carries both disabled and aria-disabled with contradictory values, such as disabled alongside aria-disabled="false", assistive technologies can report a different state than the browser enforces. Screen reader users then can't tell whether the cell or control can be used.
Use one mechanism: disabled alone on a native form control inside a cell, and aria-disabled alone on the grid, its rows, and its cells.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Uses aria-disabled rather than disabled
Reported as: Element does not support the disabled attribute
A data grid's rows and cells must convey a disabled state with aria-disabled rather than the disabled attribute.
When disabled is set on a <td>, a <tr>, or an element with the gridcell or row role, browsers ignore it. The element stays focusable and operable, and screen reader users hear nothing about it being unavailable even if it looks disabled.
Replace disabled with aria-disabled="true", and stop the row's or cell's actions in code while it's set.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Multiple Widgets
A grid cell is one stop in arrow-key navigation, so what it holds decides how many more steps users need to reach and operate its content. Cells that hold text or a single control keep the grid quick to move through.
Keeps composites out of cells
Reported as: Cell contains a composite element
A data grid's cells should not hold a composite widget that uses the arrow keys for its own operation, such as a radio group or another grid.
When a cell holds one, the arrow keys have two jobs: moving between cells and operating the widget. Keyboard users can get stuck inside the widget, or leave the cell when they meant to change its value.
Move the widget out of the grid, or replace it with a control that doesn't need the arrow keys. If it has to stay, users enter it with Enter and leave it with Escape, as Interactions Within Cell describes.
Severity: Moderate
Limits a cell to one control
Reported as: Cell contains multiple interactives
A data grid's cells should hold no more than one control.
When a cell holds two or more, keyboard users have to switch from moving between cells to moving within the cell to reach each control, an extra step the grid's navigation doesn't reveal.
Put each control in its own cell. When the controls have to share a cell, focus the cell itself during navigation and let users press Enter to reach them and the arrow keys to move between them.
Severity: Minor
Nested Tables
A grid cell holds data or a control, not another table.
Avoids nesting tables
Reported as: Unsupported table structure
Sanity check. Evinced checks that no table is nested inside a cell of a data grid, since the grid tests can't evaluate that structure. This check runs on several UI patterns.
A data grid's cells should hold content rather than another table.
When a table is nested in a grid cell, assistive technologies present it as a separate table with its own rows and columns. Users then have to keep track of two sets of headers and positions at once, and can easily lose their place.
Move the data into the grid's own rows, or place the table beside the grid rather than inside it. Use CSS if you need to arrange content within a cell.
Relationships
A data grid has a table's containment hierarchy: the grid owns its row groups and rows, each row group owns its rows, and each row owns its cells. Data cells in a grid are grid cells, a specialization of the table's cell that users can focus and operate.
A native <table role="grid"> expresses the hierarchy through its elements, and a grid built from generic elements nests rowgroup, row, and cell elements the same way. The DOM hierarchy carries every relationship, so the parts of a grid don't need aria-controls or aria-owns to relate to each other.
aria-owns
A data grid is especially likely to use aria-owns. Grids that show large data sets often render rows through a virtualization library, and some libraries place those rows outside the grid's DOM. Owned rows also break the grid's arrow-key navigation, which follows the DOM rather than the accessibility tree.
Avoids aria-owns
Reported as: Avoid aria-owns
General check. Evinced checks that a data grid and its grid cells don't use aria-owns to claim elements elsewhere in the document as their children. This check runs on every UI pattern, not only on data grids.
A data grid should hold its rows, and its grid cells their content, in the DOM rather than own them with aria-owns.
When rows rendered elsewhere are owned, assistive technology places them after the grid's own rows, wherever they appear on screen, while the arrow keys move focus in DOM order. Screen reader users then hear rows out of sequence, or find rows the grid's navigation never reaches.
Render the rows inside the grid or its row groups, using a virtualization library that renders rows in place, or portal the whole grid. Keep a cell's content inside the cell. When some rows must be owned, list every row in aria-owns in the order users should meet them.
Owns only conforming roles
Reported as: Invalid aria-owns values
General check. Evinced checks that every element a data grid references in aria-owns has a role the grid may own. This check runs on several UI patterns.
When a data grid uses aria-owns, each element it references should have the row or rowgroup role.
When an owned element has another role, the grid's structure no longer adds up. Row counts and cell positions come out wrong, and screen reader users hear a garbled or incomplete grid. An element owned by two containers leaves the structure undefined.
Reference only rows and row groups, and own each element from one container. Better still, move them into the grid in the DOM and remove aria-owns.
Operation
A data grid is operated with a pointer and a keyboard, and its cells have two keyboard modes. In navigation mode, the arrow keys and the grid's other navigation keys move focus between cells. In interaction mode, focus is inside one cell's content, and the keys operate what the cell holds.
Where navigation puts focus depends on what the cell holds. Two cell designs work best:
- A cell that holds one control that doesn't use the arrow keys, such as a button, link, checkbox, or switch. Navigation moves focus straight to the control, and users operate it without leaving navigation mode.
- A cell that holds text or a single graphic. Navigation moves focus to the cell.
Other designs, such as a cell with an editable field, several controls, or a control that uses the arrow keys, are allowed but are harder to use. Navigation moves focus to the cell, and users switch to interaction mode with Enter or F2, or by typing a letter or number when the cell holds a text field.
In interaction mode, the grid behaves much like a table, with every control in it a stop for the Tab key. Escape switches back to navigation mode. A second F2, or a second Enter in a single-line text field, can also switch back.
Pointer Operation
- Clicking a cell moves focus to it, and later arrow-key navigation starts from that cell.
- Clicking a control in a cell, such as a checkbox or link, operates it directly, without a separate step to enter the cell.
- Clicking a sortable column header changes the sort order, and the header's
aria-sortvalue updates to match.
Keyboard Operation
The Tab key moves focus into the grid, and pressing it again moves focus out to the next element on the page. Shift + Tab does the same in the other direction. The following keys move focus between cells in navigation mode:
| Key | Function |
|---|---|
| Right Arrow | Moves focus one cell right, and stops at the end of the row |
| Left Arrow | Moves focus one cell left, and stops at the start of the row |
| Down Arrow | Moves focus one cell down, and stops at the bottom of the column |
| Up Arrow | Moves focus one cell up, and stops at the top of the column |
| Home | Moves focus to the first cell in the current row |
| End | Moves focus to the last cell in the current row |
| Control + Home | Moves focus to the first cell in the grid |
| Control + End | Moves focus to the last cell in the grid |
| Page Down (optional) | Moves focus down by a set number of rows, usually the number visible |
| Page Up (optional) | Moves focus up by a set number of rows, usually the number visible |
A layout grid's navigation differs in a few ways. Since its cells' positions carry little meaning, the arrow keys may wrap: Right Arrow on the last cell in a row can move to the first cell in the next row, and Down Arrow on the last cell in a column to the top of the next column, so users can go through the whole set with one key. A data grid doesn't wrap, since moving to another row or column without warning would misrepresent where users are in the data.
In a layout grid, Page Down, Page Up, Control + Home, and Control + End are optional. In a layout grid with one column, or fewer than three cells per row, Home and End may move to the first and last cell in the grid.
A grid that supports selection commonly uses the following keys in navigation mode:
| Key | Function |
|---|---|
| Shift + Space | Selects the row that holds focus. In a grid with a column of checkboxes for selecting rows, it checks the row's box without moving focus to it |
| Control + Space | Selects the column that holds focus |
| Control + A | Selects every cell |
| Shift + Right Arrow, Shift + Left Arrow | Extends the selection one cell right or left |
| Shift + Down Arrow, Shift + Up Arrow | Extends the selection one cell down or up |
Cut, copy, and paste use the platform's usual keys, such as Control + C, or Command + C on macOS.
The following keys switch between modes and work inside a cell:
| Key | Function |
|---|---|
| Enter or F2 | Switches to interaction mode, moving focus to the editable content or first control in the cell |
| Letter and number keys | Switch to interaction mode in a cell with a text field, and start typing in it |
| Escape | Returns to navigation mode, moving focus back to the cell |
| F2, or Enter in a single-line text field | Can also return to navigation mode, when pressed a second time |
| Enter or Space | Operates the focused control in the cell |
| Arrow keys | In a cell with several controls, move to the next or previous control in the cell. Otherwise, operate the focused control, such as moving the cursor in a text field |
| Tab, Shift + Tab | Move to the next or previous control in the grid. Some grids keep focus among the controls in the cell instead |
Focus Sequence
A data grid is one stop in the page's tab order: the Tab key moves focus into the grid in one step and out of it in the next. With a roving tabindex, only the active cell, or the control in it, has tabindex="0". With aria-activedescendant, the grid container is the tab stop and points at the active cell.
Reachable with the Tab key
Reported as: Composite not in focus sequence; Data grid not focusable
A data grid must include its container or one of its cells in the page's tab order.
When no element in the grid can be reached with the Tab key, keyboard users can't move focus into the grid at all, so they can't read its cells or operate anything in them.
Set tabindex="0" on one cell, or on the control inside it, with a roving tabindex. With aria-activedescendant, set tabindex="0" on the grid container.
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Lands on the first or last active cell
Reported as: Composite unpredictable focus
When focus enters a data grid, it must land on the cell that was last active, or on the first cell the first time.
When focus lands somewhere else, keyboard and screen reader users lose their place, and have to work out where in the grid they are before they can continue.
Keep track of the active cell, and give it the grid's one tabindex="0", so the Tab key returns to it. Put it on the first cell until the user has moved.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Gives its items a single tab stop
Reported as: Redundant focusable children
A data grid must have only one element in the page's tab order at a time, with the arrow keys reaching the rest.
When several cells or controls in the grid have tabindex="0", keyboard users have to press the Tab key at each of them to get past the grid, and lose the shortcut a grid exists to provide.
Give tabindex="0" to the active cell, or its control, only. Give every other cell and control in the grid tabindex="-1", and move the 0 as focus moves.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Focus Within Cell
When arrow-key navigation reaches a grid cell, focus lands on the cell or on the control inside it, depending on what the cell holds. The Examples show the second case: each cell holds one button, so focus goes to the button.
Focuses the cell unless it holds only a simple control
Reported as: Focus lands within grid cell with multiple elements
A data grid cell should take focus itself when users navigate to it, unless its only content is a simple control: one control that doesn't use the arrow keys, such as a button, link, checkbox, or switch. The cell takes focus when it holds any of the following:
- Text, a graphic, or other non-interactive content, alone or beside a control
- More than one control
- A single control that uses the arrow keys, such as a text field or a slider
When focus goes to an element inside such a cell, users skip the rest of what the cell holds, or land on one of several controls without learning that the others are there. A control that uses the arrow keys also takes them away from moving between cells.
Move focus to the cell element, and let users press Enter or F2 to reach what's inside it.
Severity: Moderate
Focuses the control in the cell
Reported as: Redundant focus on grid cell
A data grid cell whose only content is a simple control (one that doesn't use the arrow keys, such as a button, link, checkbox, or switch) should pass focus straight to that control.
When focus stops on the cell instead, keyboard users need an extra key press to reach a control that's the cell's only content.
Move focus to the control inside the cell when arrow-key navigation reaches it.
Severity: Moderate
Interactions Within Cell
When a grid cell holds interactive content, users switch between two modes: moving from cell to cell with the arrow keys, and operating the content inside one cell. While focus is inside a cell's content, the keys belong to that content, and the grid's navigation keys don't move focus between cells until users return to navigation mode, usually with Escape.
Steps into what a cell holds
Reported as: Focus does not enter grid cell
A data grid cell that takes focus itself but holds interactive content must let keyboard users move focus into that content with a key press: Enter or F2, or a letter or number key when the cell holds a text field.
When focus stays on the cell after the key press, keyboard users can see the control inside it but can't reach or operate it, such as typing in a text field.
Handle Enter and F2 on the cell by moving focus to the editable content or the first interactive element inside it. In a cell with a text field, let a letter or number key move focus into the field and start the entry.
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Keeps focus in the cell until navigation resumes
Reported as: Grid cell not focused after action
Once focus is inside a data grid cell's content, it must stay in the cell until users press the key that returns to navigation mode, usually Escape, or move on to another control with Tab or Shift + Tab.
When focus moves to another cell or out of the grid on a key the content uses, such as an arrow key in a text field, or after users operate a control in the cell, keyboard users can't finish what they started and lose their place.
While focus is inside the cell, let the content handle its own keys, and don't pass them on to the grid's navigation. Keep focus in the cell after users operate a control there, and move it back to the cell only on Escape, or on F2 or Enter when the grid uses them to leave the cell.
Severity: Critical
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Restores navigation on Escape
Reported as: Cannot restore grid navigation
When users press Escape inside a data grid cell's content, the grid should return to navigation mode, so the arrow keys move between cells again.
When Escape leaves focus in the content, or moves it to another cell, keyboard users can't go on moving through the grid with the arrow keys from where they were.
Handle Escape in the cell's content by moving focus back to the cell that holds it.
Severity: Moderate
Holds focus in the cell on Escape
Reported as: Grid cell lost focus on Escape
When users press Escape inside a data grid cell's content, focus must stay in the grid and return to that cell.
When Escape moves focus out of the grid, keyboard users lose their place and have to find the grid, and then the cell, again.
Make the cell focusable with tabindex="-1", and move focus to it when users press Escape.
Severity: Moderate
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Keyboard Accessible
Users reach every cell in a data grid with the arrow keys, because screen reader users usually read a grid by moving focus through it. Header cells are the one exception: they need focus only when they provide a function, such as sorting.
Reaches every cell
Reported as: Unreachable grid cells
Every data cell in a data grid, and every header cell that provides a function, must be reachable with the arrow keys.
When navigation skips a cell, keyboard users can't read or operate what it holds, and screen reader users may not know the cell exists.
Make every cell, or the one control inside it, focusable with tabindex="-1" or through aria-activedescendant. Include spanned and newly rendered cells in the logic that works out which cell each arrow key moves to.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
Holds focus at every cell
Reported as: Focus inconsistently leaves data grid
No cell in a data grid should let an arrow key move focus out of the grid.
When the arrow keys keep focus inside the grid everywhere but one cell, an arrow key there drops keyboard users somewhere else on the page. They've learned that the arrow keys keep them in the grid, so they have to notice what happened, then find the grid, and their place in it, again.
The cell usually differs from the rest in a way the navigation code doesn't account for: it spans rows or columns, it's empty, it was added after the grid first rendered, or a control in it handles the arrow keys itself.
Work out each move from the cells actually present, so spanned, empty, and added cells resolve to a real position. At the end of a row or column, leave focus where it is in a data grid. A layout grid may wrap to the next row or column instead, as Keyboard Operation describes. Either way, don't pass the key to the browser.
Severity: Minor
Keyboard Navigation
A data grid moves focus between cells with the arrow keys, Home, End, Control + Home, and Control + End, and keeps it inside the grid. Handle these keys on keydown: the deprecated keypress event doesn't fire for them in current browsers.
The following example shows the shape of a keydown handler for navigation mode. The focusCell, focusFirstCellInRow, and focusLastCellInRow helpers stand for your own code that moves the roving tabindex and sets focus.
grid.addEventListener("keydown", (event) => {
const cell = event.target.closest(
'[role="gridcell"], td, th'
);
switch (event.key) {
case "ArrowRight":
focusCell(cell, 0, +1);
break;
case "ArrowLeft":
focusCell(cell, 0, -1);
break;
case "ArrowDown":
focusCell(cell, +1, 0);
break;
case "ArrowUp":
focusCell(cell, -1, 0);
break;
case "Home":
if (event.ctrlKey) {
focusCell(null, "first");
} else {
focusFirstCellInRow(cell);
}
break;
case "End":
if (event.ctrlKey) {
focusCell(null, "last");
} else {
focusLastCellInRow(cell);
}
break;
default:
return;
}
event.preventDefault();
});Keeps focus in the structure
Reported as: Focus left datagrid unexpectedly
Each of a data grid's navigation keys must move focus to another cell or leave it where it is, never move it out of the grid. A data grid stops at the end of each row and column, and a layout grid may wrap to the next one.
When an arrow key, Home, End, Control + Home, or Control + End moves focus outside the grid, keyboard users lose their position, and the order in which they read the grid breaks.
Handle every navigation key in the grid's keydown handler. Call event.preventDefault(), and move focus to the target cell yourself rather than leaving the key to the browser.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.2 Meaningful Sequence (A) | When the sequence in which content is presented affects its meaning, a correct reading sequence can be programmatically determined. |
| EN 301 549 | 9.1.3.2 Meaningful Sequence | Same as the WCAG text. |
Moves focus to the next cell
Reported as: Failed next cell keyboard navigation
A data grid should move focus to the next cell in the row with the Right Arrow key, and to the next cell in the column with the Down Arrow key.
When these keys don't move focus, keyboard users can enter the grid but can't move forward through its data.
Handle the Right Arrow and Down Arrow keys in the grid's keydown handler, moving focus to the neighboring cell and preventing the browser's default scrolling.
Severity: Minor
Moves focus to the previous cell
Reported as: Failed previous cell keyboard navigation
A data grid should move focus to the previous cell in the row with the Left Arrow key, and to the previous cell in the column with the Up Arrow key.
When these keys don't move focus, keyboard users can move forward through the grid but can't go back to data they've passed.
Handle the Left Arrow and Up Arrow keys the same way as the Right Arrow and Down Arrow keys, in the opposite direction.
Severity: Minor
Moves focus between cells
Reported as: Grid arrow navigation failed
A data grid should move focus one cell at a time with the arrow keys, along both the row and the column.
When none of the arrow keys moves focus, keyboard users have no way through the grid's data beyond the cell they entered on.
Handle all four arrow keys in the grid's keydown handler. Work out the target cell from the current cell's row and column, and move focus there.
Severity: Minor
Follows the text direction
Reported as: RTL navigation not supported
Sanity check. Evinced checks that a data grid's Right Arrow and Left Arrow keys move focus in the reading direction when its content runs right to left.
A data grid's Right Arrow and Left Arrow keys should move focus in the reading direction of its content.
When a right-to-left grid still treats Right Arrow as forward, every press moves focus the opposite way from the one users expect.
Resolve Right Arrow and Left Arrow against the content's computed text direction rather than hard-coding which one advances. Up Arrow and Down Arrow are unaffected.
aria-sort
A data grid that users can sort sets aria-sort on the header of the sorted column or row, so assistive technologies can convey the sort order along with the header. The header holds a button that changes the sort. In a grid, that button takes part in the grid's focus management like any other cell content: users reach it with the arrow keys, and it has tabindex="-1" until it's the active control.
In the following example, the grid is sorted by its Price column in ascending order.
<tr>
<th scope="col">Product</th>
<th
scope="col"
aria-sort="ascending">
<button
type="button"
tabindex="-1">
Price
</button>
</th>
</tr>Sorts from the keyboard
Reported as: Table sort not accessible to keyboard
A data grid's sortable header must let keyboard users reach and operate its sort control.
When sorting responds only to a pointer click on the header, or arrow-key navigation skips the header row, keyboard users can't change the grid's order.
Put a <button> inside the header cell, include it in the grid's arrow-key navigation, and run the sort from its click event, which also fires for Enter and Space. Keep aria-sort on the header cell, not on the button.
Severity: Serious
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 2.1.1 Keyboard (A) | All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints. |
| EN 301 549 | 9.2.1.1 Keyboard | Same as the WCAG text. |
| Section 508 | 1194.21(a) | When software is designed to run on a system that has a keyboard, product functions shall be executable from a keyboard where the function itself or the result of performing a function can be discerned textually. |
| 508 Trusted Tester | 2.1.1-keyboard-access, 4.A | All functionality can be accessed and executed using only the keyboard. |
Marks one sorted column at a time
Reported as: Ambiguous mult-sort levels
A data grid must declare an active sort on no more than one header at a time.
When more than one header carries aria-sort set to ascending, descending, or other, screen reader users can't tell which column decides the order. ARIA has no way to express sort priority across several columns.
Set an active aria-sort value on the header of the primary sort column only. Remove the attribute from the other headers, or set it to none.
Severity: Minor
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 1.3.1 Info and Relationships (A) | Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. |
| EN 301 549 | 9.1.3.1 Info and Relationships | Same as the WCAG text. |
| Section 508 | 1194.22(h) | Markup shall be used to associate data cells and header cells for data tables that have two or more logical levels of row or column headers. |
Uses a defined sort value
Reported as: Aria-sort invalid value
A data grid header's aria-sort value must be one of the four the property defines: ascending, descending, other, or none.
When the value is misspelled or made up, assistive technologies ignore it, and users get no sort information for that header.
Use ascending or descending for those orders, other for any other sort algorithm, and none, or no attribute, for an unsorted header. Update the value in the same step that re-sorts the rows.
Severity: Minor
| Standard | Success Criterion | Reference |
|---|---|---|
| WCAG 2.2 | 4.1.2 Name, Role, Value (A) | For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to assistive technologies. |
| EN 301 549 | 9.4.1.2 Name, Role, Value | Same as the WCAG text. |
| Section 508 | 1194.21(d) | Sufficient information about a user interface element including the identity, operation and state of the element shall be available to assistive technology. When an image represents a program element, the information conveyed by the image must also be available in text. |
Puts the sort state on a header
Reported as: Aria-sort on a table cell
A data grid should declare its sort order on the header cell of the sorted column or row.
When aria-sort is on a grid cell, it has no defined meaning, and assistive technologies ignore it or handle it unpredictably. Screen reader users then don't learn how the grid is sorted.
Move aria-sort to the <th>, or the element with role="columnheader" or role="rowheader", that heads the sorted column or row.
Severity: Minor
Event Listener Type
When script in a data grid responds to keys, it listens for keydown or keyup. The keypress event is deprecated, and browsers don't fire it for many keys, including the arrow keys, Home, End and Escape.
Avoids the keypress event
Reported as: Deprecated keypress listener
A data grid's key handling should use keydown or keyup, not keypress.
When a handler listens for keypress, keys the browser doesn't send through that event go unanswered, and keyboard users find the data grid ignoring keys it's meant to respond to.
Replace each keypress listener with a keydown listener, and read the key from event.key.
Severity: Minor
FAQ
Which WCAG success criteria do the data grid issues relate to?
Most data grid issues relate to 1.3.1 Info and Relationships, 2.1.1 Keyboard, and 4.1.2 Name, Role, Value. Under 2.1.1, the grid is reachable with the Tab key and returns focus to the last active cell, a sortable header works from the keyboard, and users can step into a cell's content and operate it there.
Under 4.1.2, the grid and its cells carry their roles, cells have accessible names, aria-sort has a defined value, and the disabled state is declared only where it's supported and one way only.
Under 1.3.1, the grid has a name, rows in the grid and its row groups, and cells in each row, all with their roles. Its header cells have a scope that matches their span, spanning headers are built natively, only one column is sorted, and its row and column counts and indexes are correct. The grid is also one tab stop, and every cell can be reached with the arrow keys.
Under 1.3.2 Meaningful Sequence, the navigation keys and Escape keep focus inside the grid, and its footer rows sit in one section.
The rest are best practices, including aria-sort on a header, aria-readonly only in an editable grid, what a cell holds and where focus lands in it, and moving between cells with the arrow keys. A single header section, a caption with text, and a caption on a native grid are best practices too. So are indexes that come with a total, run in order, stay in range, and start at 1.
Why are some data grid issues marked as Critical and others as Minor?
Severity reflects how much an issue gets in users' way. Critical issues stop users outright:
- A container without the
gridrole, or data cells without thegridcellrole - Cells with no accessible name
- A grid that can't be reached with the Tab key
- A cell whose content users can't move focus into, or whose keys move focus away
Serious issues mislead users or leave parts of the grid out of reach:
- Rows or cells without their roles, or a grid, row group, or row with nothing in it
- Spanning headers that aren't associated with their cells
- Missing column indexes
- A sortable header that doesn't work from the keyboard
- A disabled state that the role doesn't support, that's declared two ways, or that's set with
disabledon a row or cell - Focus that enters the grid somewhere other than the last active cell
- A cell the arrow keys can't reach
- Navigation keys that move focus out of the grid
Moderate issues cost users extra steps, their place, or the context they need:
- A composite widget that uses the arrow keys inside a cell
- More than one tab stop in the grid
- Focus that lands on the wrong element in a cell
- An Escape key that doesn't return focus to the cell
- A grid with no name, an empty caption, or no caption on a native grid
- No header cells in a data grid, or header cells without
scope - Footer rows split across several sections
- Row or column totals lower than what's rendered
Minor issues are smaller obstacles:
- An undefined
aria-sortvalue, oraria-sorton a data cell aria-readonlyin a grid that has no editing- More than one control in a cell
- A cell that lets an arrow key move focus out of the grid
- Arrow keys that don't move focus between cells
- Header rows split across several sections
- More than one sorted column
- Indexes without a total, out of order, out of range, below 1, or set on both rows and cells
The aria-owns checks, the check on where the grid role sits, the check for nested tables, and the check on right-to-left navigation carry no severity: Evinced reports them as general and sanity checks.
Related Patterns
- Table: the static structure a data grid builds on, for data users read and compare, with at most a few controls
- Radio Group: a composite widget that uses the arrow keys itself, so it doesn't belong in a grid cell
Recommended Reading
- Grid Pattern | W3C ARIA Authoring Practices Guide
- grid role | W3C WAI-ARIA
- Grid and Table Properties | W3C ARIA Authoring Practices Guide
- ARIA: grid role | MDN
- Grids Part 1: To Grid or Not to Grid | Sarah Higley
- Grids Part 2: Semantics | Sarah Higley
- ARIA Grid As an Anti-Pattern | Adrian Roselli
- Don't Turn a Table into an ARIA Grid Just for a Clickable Row | Adrian Roselli