Table
UI pattern
table role, has an accessible name, exposes the role of every row group, row, and cell, and has header cells that label each data cell.Users find a value by its position under a column header and beside a row header. A table is a static structure: the table and its cells don't take focus or offer any interaction of their own. Screen reader users move through it with table-reading commands.
A cell can hold other content, and a control in a cell, such as a link or a sort button, works as it would outside the table.
A table is built from nested parts. It holds row groups for its header, body, and footer. Each row group holds rows, and each row holds cells. A cell is either a header cell, which labels its column or row, or a data cell, whose meaning comes from the headers that label it.
A data grid uses the same rows and cells but is a composite widget: the whole grid is a single stop in the tab order, and keyboard users move between its cells with the arrow keys. A table can hold interactive content too, but each control in it is its own tab stop. When many cells hold controls, a data grid is the better choice.
When to Use
Use a table when:
- The content has a two-dimensional relationship, and each value makes sense only with both its row header and its column header.
- Users need to compare values across items, such as a feature matrix, a pricing comparison, or a specification sheet.
- The data is a schedule or timeline, with time periods in one dimension and events or values in the other.
- Few cells hold interactive controls, so they can all be in the tab order.
Do not use a table when:
- You only want to arrange content visually in columns, with no relationship between rows and columns. Use CSS layout instead. A layout table fills the accessibility tree with rows and cells that mean nothing, and if you'd have to remove its semantics with
role="presentation"orrole="none", the content isn't a table. - Many cells would hold interactive controls. Each one is its own stop in the tab order, so keyboard users press the Tab key many times to get past the table. Use a data grid instead: it's a single tab stop, and users move between its cells with the arrow keys.
Examples
Code in this section is meant to demonstrate techniques rather than requirements and lacks features needed in a production application.
Prefer native HTML table elements whenever you can. They convey the table's structure to assistive technologies without any ARIA.
Native HTML Table
The native elements carry the table's roles and relationships, so assistive technologies get its structure without extra ARIA. The <caption> gives the table its accessible name, and scope on each <th> says whether it heads a column or a row.
Native table HTML
<table>
<caption>Q1 sales by region</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">January</th>
<th scope="col">February</th>
<th scope="col">March</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North</th>
<td>$12,400</td>
<td>$9,800</td>
<td>$14,200</td>
</tr>
<tr>
<th scope="row">South</th>
<td>$8,500</td>
<td>$7,100</td>
<td>$9,300</td>
</tr>
</tbody>
</table>Each element has one job in the structure:
| Element | Purpose |
|---|---|
<table> | Root container for the table |
<caption> | Provides the table's accessible name, rendered as visible text |
<thead> | Groups the header rows |
<tbody> | Groups the body rows |
<tfoot> | Groups the footer rows |
<tr> | A table row, containing one or more cells |
<th> | A header cell |
<td> | A data cell |
Custom ARIA Table
When you can't use native table elements, rebuild the same structure with ARIA roles on generic elements.
Custom ARIA table HTML
<div role="table" aria-labelledby="sales-caption">
<div role="caption" id="sales-caption">Q1 sales by region</div>
<div role="rowgroup">
<div role="row">
<span role="columnheader">Region</span>
<span role="columnheader">January</span>
<span role="columnheader">February</span>
</div>
</div>
<div role="rowgroup">
<div role="row">
<span role="rowheader">North</span>
<span role="cell">$12,400</span>
<span role="cell">$9,800</span>
</div>
</div>
</div>Each role stands in for one native element:
| Native HTML element | ARIA role |
|---|---|
<table> | role="table" |
<caption> | role="caption" |
<thead>, <tbody>, <tfoot> | role="rowgroup" |
<tr> | role="row" |
<th scope="col"> | role="columnheader" |
<th scope="row"> | role="rowheader" |
<td> | role="cell" |
A custom caption doesn't name the table on its own. Put the element with role="caption" first inside the table, and point the table's aria-labelledby at it.
Structure and Semantics
Screen reader users read a table cell by cell, and at each cell they can hear the headers that label it and its row and column position. That works only when every part of the table exposes its role, the parts nest correctly, and each data cell can be matched to its headers.
Native table elements convey all of this without ARIA, as Native HTML Table shows. A custom table carries the ARIA roles table, rowgroup, row, columnheader, rowheader, and cell, and caption for its caption, as Custom ARIA Table shows. A few ARIA properties add what the markup can't say on its own: aria-sort on a sorted header, and the count and index properties on a table that renders only part of its data.
Table Structure
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 table must place all its footer rows in a single <tfoot> element.
When a table 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 table should place all its header rows in a single <thead> element.
When a table 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 table holds its rows directly or inside row groups, and assistive technologies report the table's size from the rows they find.
Gives members the required role
Reported as: No group members with required role
A table must contain at least one row, directly or inside a row group.
When a table has no descendants with the row role, screen reader users hear that a table is there but find nothing to read, and they can't tell whether the data failed to load or doesn't exist.
Render the table only when it has rows, and show a message outside it when there's no data. In a custom table, 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 table, such as its header or its body.
Gives members the required role
Reported as: No group members with required role
A table'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 data, 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 table row must contain at least one cell with the cell, columnheader, or rowheader role.
When a row has no cells, or its children are generic elements, screen reader users moving through the table reach a row with nothing in it, and its columns no longer line up with the rows around it.
Put a <td> or <th> in every row, or give each cell in a custom row the cell, 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
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 table, 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 custom table, aria-colspan and aria-rowspan set the same spans, but they don't replace the header association a native table provides.
Builds spanned headers natively
Reported as: Complex headers in ARIA table
A table with header cells that span rows or columns should be built with native table elements.
When a custom table 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.
Use a native <table>, with colspan or rowspan and scope on each spanning <th>, so the browser can apply its own header association. If the table has to be a custom table, 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 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 table must have header cells that identify what each column or row holds.
When a table has no <th> elements and no elements with role="columnheader" or role="rowheader", screen reader users hear values with nothing to say what they mean.
Mark the cells that label columns or rows as header cells: <th> in a native table, or role="columnheader" and role="rowheader" in a custom table. If the table is there only for layout, replace it with CSS layout.
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 table must declare with scope whether it labels a column or a row.
When scope is missing, browsers guess the association from the table'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
Every part of a table carries the role for its position: table on the container, rowgroup on each header, body, or footer group, row on each row, and columnheader, rowheader, or cell on each cell. Native table elements supply these roles, and a custom table sets each one explicitly, following the mapping in Custom ARIA Table.
Note: Changing the CSS display value of table elements, for example to flex, grid, or block in a responsive layout, can remove their table roles in some browsers. If you restyle a native table this way, restore its semantics by adding the matching roles from Custom ARIA Table to each element.
Has the correct role
Reported as: Element has incorrect role
Each part of a table must expose the role that matches its position in the structure.
When a part carries the wrong role, or none, assistive technologies misreport the table. When a header is exposed as a cell, screen reader users don't hear it with the data it labels, and a row exposed as a generic element breaks their navigation by row and column.
Use native table elements, or set each role from the mapping in Custom ARIA Table.
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 table role sits on the outermost element that contains the table's rows, rather than on one of its descendants. This check runs on every UI pattern, not only on tables.
A table's table role should sit on the outermost element that contains its rows.
When the role is set on a descendant rather than on the container, the container isn't identified as a table, and rows outside the element carrying the role can fall outside the structure assistive technologies present. Screen reader users then don't reach those rows as part of the table.
Move role="table" to the element that contains every row, or use a native <table> as the container.
Accessible Name
Each 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 or image has no name unless you provide one.
Has an accessible name
Reported as: Accessible name
A table cell must have an accessible name that conveys its content.
When a cell holds only an icon or image with no text alternative, screen reader users hear an empty cell. If it's a header cell, every data cell it labels loses its context too.
Give an image in the cell alt text, or add visually hidden text to the cell. Say what the icon means, such as "Available" rather than "Check mark".
Severity: Moderate
| 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 table has an accessible name that says what data it holds, separate from the names of its header cells. Screen readers announce the name when users enter the table and list it among the page's tables, so it's how users tell one table from another before reading any cells.
Names a container that needs one
Reported as: Missing contextual labeling
A table, native or custom, must have an accessible name that identifies the data it holds.
When a table has no name from any source, screen reader users hear only that they've entered a table, with no clue which one or what it's for. On a page with several tables, every one is listed the same way.
Give the table a name with a <caption>, or with aria-labelledby pointing at the visible heading or caption that introduces it. 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 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 table identifies it, users who reach it by jumping from table to table find an unnamed entry and have to read its headers to learn what it holds.
Add a <caption> as the first child of the <table>. If a <caption> can't be used, give the table an accessible name by setting aria-labelledby to reference visible text, or by setting aria-label.
Severity: Moderate
Fills the caption it declares
Reported as: Table has empty caption
A table's caption should hold text that identifies the table, or not be there at all.
When the caption is empty or holds only whitespace, the table gets an empty accessible name. Screen reader users get no indication of what the table holds, even though the markup looks as if it names the table.
Write a short phrase in the caption that says what the table holds. If you don't want a caption, remove the element and name the table with aria-labelledby or aria-label. In a template, render the caption only when there's text for it.
Severity: Moderate
Cell Index
When a table renders only part of its data, such as one page or the rows in view of a virtual scroller, it 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 reader table-reading commands skip rows or columns, or stop working.
The following example shows the first two rows of a table with 500 rows and 6 columns. The same attributes work on the table, row, and cell elements of a custom table.
<table aria-rowcount="500" aria-colcount="6">
<caption>Products</caption>
<thead>
<tr aria-rowindex="1">
<th scope="col" aria-colindex="1">Product</th>
<th scope="col" aria-colindex="2">Price</th>
</tr>
</thead>
<tbody>
<tr aria-rowindex="2">
<td aria-colindex="1">Widget A</td>
<td aria-colindex="2">$4.99</td>
</tr>
<!-- Rows 3 to 500 are rendered as the user scrolls -->
</tbody>
</table>Indexes the columns it counts
Reported as: Missing aria-colindex
When a table 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 table 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 table.
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 table'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 table contradicts itself, and screen reader users are told the table 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 table'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 table 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 table that uses aria-colindex should declare its total number of columns with aria-colcount.
When the table has no aria-colcount, a column position has nothing to count against, so screen reader users hear a position without learning how wide the table is.
Add aria-colcount to the table 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 table that uses aria-rowindex should declare its total number of rows with aria-rowcount.
When the table has no aria-rowcount, a row position has nothing to count against, so screen reader users hear a position without learning how long the table is.
Add aria-rowcount to the table 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 table'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 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 table'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 table 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 table'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
Disabled
A static table has nothing to disable. The table and its row groups, rows, and cells aren't interactive. When something in a table is unavailable, the disabled state belongs on the control inside the cell, such as the button in a sortable header.
Disables only when role allows
Reported as: Component does not support disabled state
A table must declare a disabled state only on the controls inside its cells, not on the table, its row groups, rows, or cells.
When a table or one of its parts carries aria-disabled, assistive technologies ignore it or report a state users can't act on. Users may then think a row is unavailable when nothing in it has changed.
Remove the attribute, and disable the control inside the cell instead.
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 table 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, as the section intro describes. When a control inside the table is unavailable, set the disabled state on that control, using an element or role that supports it.
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 control in a table must 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 control can be used.
Use one mechanism: disabled alone on a native form control, or aria-disabled alone on a custom one.
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. |
Keeps disabled off table parts
Reported as: Element does not support the disabled attribute
A table's parts must not carry the HTML disabled attribute.
When disabled is set on a <table>, <tr>, <th>, or <td>, or on an element with a table role, browsers ignore it. The controls inside stay operable, and screen reader users hear nothing about the part being unavailable even if it looks disabled.
Remove the attribute, and set disabled on the form control inside the cell instead.
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. |
Nested Tables
A table cell holds data, not another table.
Avoids nesting tables
Reported as: Unsupported table structure
Sanity check. Evinced checks that no table is nested inside a cell of another table, a structure its table tests can't evaluate. This check runs on several UI patterns.
A table cell should hold content rather than another table.
When a table is nested in a cell, assistive technologies present the inner table 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 outer table, or place the inner table beside it rather than inside it. Use CSS if you need to arrange content within a cell.
Relationships
A table's structure is a containment hierarchy: the table owns its row groups and rows, each row group owns its rows, and each row owns its cells. Native table elements express this by nesting, with <tr> inside <thead>, <tbody>, or <tfoot>, and <th> or <td> inside <tr>. A custom table nests its rowgroup, row, and cell elements the same way, and assistive technologies find each row's cells among its descendants.
The parts of a table don't need aria-controls or aria-owns to relate to each other. The DOM hierarchy carries every relationship.
aria-owns
A table doesn't need aria-owns when its rows are nested inside it in the DOM. The attribute comes up only when a script renders rows outside the table, as some virtualization libraries do, and it can then claim those rows, but only in the accessibility tree.
Avoids aria-owns
Reported as: Avoid aria-owns
General check. Evinced checks that the table, its row groups, and its rows 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 tables.
A table and its row groups should hold their rows in the DOM rather than own them with aria-owns.
When rows rendered elsewhere are owned, assistive technology places them after the table's own rows, wherever they appear on screen. Assistive technology that doesn't honor the attribute reports them outside the table. Screen reader users then hear rows out of sequence, or find rows that aren't counted or reachable with table navigation.
Render the rows inside the table or its row groups, using a virtualization library that renders rows in place, or portal the whole table. 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 the table or a row group references in aria-owns has a role it may own. This check runs on several UI patterns.
When a table uses aria-owns, each element it references should have the row or rowgroup role, and a row group should reference only rows.
When an owned element has another role, the table's structure no longer adds up. Row counts and positions come out wrong, and screen reader users hear a garbled or incomplete table. An element owned by two containers, or two containers that own each other, leaves the structure undefined.
Reference only rows and row groups, and own each element from one container. Better still, move them into the table in the DOM and remove aria-owns.
Operation
A table offers no keyboard interaction of its own: neither the table nor its cells take focus. Any interactive content in its cells, such as a link, a button, or the sort control in a header cell, must be in the tab order and operable from the keyboard, as it would be outside the table. Keyboard users reach each control in turn with the Tab key.
aria-sort
A table 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, and that button, not the header, is what users focus and operate.
In the following example, the table is sorted by its Price column in ascending order.
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col" aria-sort="ascending">
<button type="button">Price</button>
</th>
<th scope="col">
<button type="button">Rating</button>
</th>
</tr>
</thead>Sorts from the keyboard
Reported as: Table sort not accessible to keyboard
A sortable header must let keyboard users reach and operate its sort control.
When sorting responds only to a pointer click on the header, keyboard users can't change the table's order.
Put a <button> inside the header cell 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 table 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 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.
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 table should declare its sort order on the header cell of the sorted column or row.
When aria-sort is on a data cell, it has no defined meaning, and assistive technologies ignore it or handle it unpredictably. Screen reader users then don't learn how the table 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 table 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 table'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 table 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 table issues relate to?
Most relate to 1.3.1 Info and Relationships: naming the table, rows in the table and its row groups, cells in each row, header cells with a scope that matches their span, spanning headers built natively, a single sorted column, and the row and column counts and indexes.
The roles of the table's parts, each cell's name, a valid aria-sort value, and how a disabled state is declared relate to 4.1.2 Name, Role, Value.
Sorting from the keyboard relates to 2.1.1 Keyboard, and a single footer section to 1.3.2 Meaningful Sequence.
A single header section, a caption with text, a caption on a native table, aria-sort on a header, and indexes that come with a total, run in order, stay in range, and start at 1 map to no WCAG success criterion and are best practices.
Why are some table issues marked as Critical and others as Minor?
Severity reflects how much an issue gets in users' way. Critical issues stop users outright, and a part of the table with the wrong role, or none, is the one critical issue.
Serious issues confuse users though tasks are still possible:
- A table, row group, or row with nothing in it
- Spanning headers that aren't associated with their cells
- A sort control keyboard users can't reach
- Missing column indexes
- A wrong disabled state
Moderate issues make the table harder to read or use:
- More than one footer section
- Missing header cells, or a missing
scope - A table with no name, or an empty or missing caption
- A cell with no accessible name
- Row or column totals that can't be true
Minor issues are smaller inconsistencies in the markup:
- More than one header section
- An ambiguous, invalid, or misplaced
aria-sort - Indexes without a total, out of order, out of range, set in two places, or below 1
The aria-owns checks and the checks on where the table role sits and on nested tables carry no severity: Evinced reports them as general and sanity checks.
Related Patterns
- Data Grid: a composite widget with a single tab stop, whose cells users move between with the arrow keys