Applying display: flex, display: grid, or display: block to table elements
via CSS removes their native table semantics in most browsers. If responsive
layout overrides are necessary, replace the native <table> with an ARIA table
pattern rather than restyling the native element.
Table
Description
A table is a static tabular structure that organizes data into rows and columns, allowing users to look up and compare values by their position relative to row and column headers.
When to use
Choose a table when the content has a genuine two-dimensional relationship and users need to look up or compare values by row and column. Good fits include:
- Comparison data such as feature matrices, pricing tables or specification comparisons across items.
- Schedules and timelines with rows or columns representing time periods and cells showing events or values.
- Structured datasets for any data where each cell is meaningfully associated with both a row header and a column header.
Do not choose a table for:
- Page layout: Layout tables were historically used to arrange visual
content in a grid, but CSS now provides far superior layout tools. Using a
table for layout adds structural noise to the accessibility tree and forces
screen reader users to navigate through meaningless row and cell
announcements. Similarly, applying
role="presentation"orrole="none"to strip the semantics from a data table is equally problematic—it removes all structural meaning from a table that users need to navigate. - Interactive tables: If the interaction model requires users to focus or select individual cells and navigate with arrow keys, use a Data Grid instead of a static table.
Examples
Native HTML Table
When a table is constructed with the following semantic HTML elements, assistive technologies get the information needed for their users without the need, in most cases, for additional ARIA attributes.
| 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 |
A table using semantic HTML elements
1<table>2 <caption>3 Q1 sales by region4 </caption>5 <thead>6 <tr>7 <th scope="col">Region</th>8 <th scope="col">January</th>9 <th scope="col">February</th>10 <th scope="col">March</th>11 </tr>12 </thead>13 <tbody>14 <tr>15 <th scope="row">North</th>16 <td>$12,400</td>17 <td>$9,800</td>18 <td>$14,200</td>19 </tr>20 <tr>21 <th scope="row">South</th>22 <td>$8,500</td>23 <td>$7,100</td>24 <td>$9,300</td>25 </tr>26 </tbody>27</table>
HTML attributes are often needed to convey internal table relationship information to screen readers.
Set on <th> to declare whether it heads a col, row, colgroup, or
rowgroup. Required for all <th> elements in data tables.
Cause a cell to span multiple columns or rows. For ARIA tables, use
aria-colspan or aria-rowspan instead.
Set on a sortable column or row header to indicate the current sort direction:
ascending, descending, other, or none.
Custom ARIA table
Prefer native <table> element whenever possible. It communicates tabular
structure to assistive technologies without any additional ARIA attributes. When
a native <table> cannot be used, replicate its structure using the following
ARIA roles:
| Semantic 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 native <table> receives its accessible name from its <caption> element.
For an ARIA table, use aria-label or aria-labelledby on the role="table"
element referencing a role="caption" element.
A table using ARIA roles and attributes
1<div role="table" aria-labelledby="sales-heading">2 <span id="sales-heading">Q1 sales by region</span>3 <div role="rowgroup">4 <div role="row">5 <span role="columnheader">Region</span>6 <span role="columnheader">January</span>7 </div>8 </div>9 <div role="rowgroup">10 <div role="row">11 <span role="rowheader">North</span>12 <span role="cell">$12,400</span>13 </div>14 </div>15</div>
Virtualized tables
When a table is paginated or uses virtual scrolling — not all rows or columns are
present in the DOM simultaneously — use aria-rowcount and aria-colcount on
the table element to declare the full dimensions, aria-rowindex on each row,
and aria-colindex on each cell or on each row as a column start position to
declare its position within the complete table.
The following example uses ARIA roles to construct a table. Use th
aria-rowcount, aria-colcount, aria-rowindex and aria-colindex attributes
similarly when constructing a table with semantic HTML elements.
Virtualized table example
1<div role="table" aria-rowcount="500" aria-colcount="6" aria-label="Products">2 <!-- Only rows 1–2 shown for brevity; rows 3–500 follow the same pattern -->3 <div role="row" aria-rowindex="1">4 <span role="cell" aria-colindex="1">Widget A</span>5 <span role="cell" aria-colindex="2">$4.99</span>6 </div>7 <div role="row" aria-rowindex="2">8 <span role="cell" aria-colindex="1">Widget B</span>9 <span role="cell" aria-colindex="2">$7.49</span>10 </div>11</div>
Structure and semantics
All users must be able to perceive the structure of a table and understand the relationship between its headers and data cells. The table must have a correct ARIA role, a meaningful accessible name, valid row and header structure, and accurate cell index values when virtualization is used.
Table structure and semantics are checked with the following tests.
Role
A table is composed of elements with a number of roles thar are assigned either by using the proper semantic HTML element or by using a generic element with an assigned ARIA role as shown in section Custom ARIA Table.
Element has incorrect role
| Standard | Criteria |
|---|---|
| WCAG | 4.1.2 Name, Role, Value |
| EN 301 549 | 9.4.1.2 Name, Role, Value |
| Section 508 | 1194.21(d) |
Impact: Critical
Every element within a table must carry the correct ARIA role, as specified in
the custom ARIA table section. For example, a cell that
presents column-heading information must be a <th scope="col"> or have
role="columnheader", not role="cell". An incorrect role prevents screen
readers from building an accurate representation of the table’s structure,
making it impossible for users to understand the relationship between headers
and data.
Table has Rows
No group members with required role
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Serious
A table must contain at least one row, and similarly, each row must have at
least one cell. A <table> with no <tr> children, an ARIA table lacking
role="row" elements, or rows without any data or header cells (such as <td>,
<th>, or their ARIA equivalents), fails to convey tabular structure to
assistive technologies. Make sure that every table includes at least one body
row, and every row contains at least one cell before rendering.
Accessibility Label
Missing contextual labeling
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Moderate
For a table to be understandable, both the table itself and its header cells must have accessible names.
Table accessible name
- For native
<table>elements, the preferred method is a<caption>element. If a visible caption is not desired, usearia-labeloraria-labelledbyinstead. Alternatively, keep the<caption>element and hide it visually with a.visually-hiddenCSS utility class (absolute positioning with clip-path), so sighted users do not see it but assistive technologies still announce it. - For custom ARIA tables (
role="table"), usearia-labeloraria-labelledbyreferencing arole="caption"element.
Cell accessible names
The <th> and <td> elements derive their accessible names from their text
content. If the cell contains only icons or images, provide a name with
aria-label or by including visually hidden text within the cell so that screen
reader users can access the cell information. Ensure that every header cell contains
meaningful, non-empty text.
Table has empty caption
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Moderate
A <caption> element that is present but contains no text provides no
accessible name for the table. Either populate the caption with a meaningful
description of the table’s content, or remove the element and provide an
accessible name using aria-label or aria-labelledby on the <table>
element.
Table Structure
Table has multiple headers
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
A table should have at most one <thead> section (or one role="rowgroup"
functioning as a header group). Multiple header sections create an ambiguous
reading order and may cause screen readers to announce header rows more than
once or out of sequence. Browsers typically treat a surplus <thead> as a
<tbody>, so the real-world impact is lower than for duplicate <tfoot>
elements — which is why multiple footers carry a higher severity rating than
multiple headers.
Table has multiple footers
| Standard | Criteria |
|---|---|
| WCAG | 1.3.2 Meaningful Sequence |
| EN 301 549 | 9.1.3.2 Meaningful Sequence |
| Section 508 | N/A |
Impact: Moderate
A table should have at most one <tfoot> section. Multiple footer sections
disrupt the logical reading order and may cause screen readers to announce
footer content in an unexpected sequence, making it difficult for users to
distinguish summary or total rows from body data.
Unambiguous headers
Missing table headers
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Moderate
The table contains no header cells at all. Data tables should have at least one
header row or column — using <th> elements for native tables, or
role="columnheader" / role="rowheader" for ARIA tables — so that screen
reader users can understand what each column or row represents. When this issue
is detected, further header analysis is skipped. If the table is used purely for
layout (not recommended), the absence of headers may be intentional, but layout
tables should be replaced with CSS-based layout instead.
Table headers missing scope attribute
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Moderate
Every <th> element in a data table must carry a scope attribute (col,
row, colgroup, or rowgroup) that declares which cells it headers. Without
scope, browsers use heuristics to infer the association, producing
inconsistent results across assistive technologies.
Conflicting scope spanning values
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Serious
When a <th> element has a colspan attribute, its scope must be set to
"colgroup". When it has a rowspan attribute, its scope must be set to
"rowgroup". Using scope="col" or scope="row" on a spanning header
conflicts with the actual coverage of that cell and may produce unreliable
associations across assistive technologies.
Complex headers in ARIA table
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Serious
When an ARIA table contains header cells that span multiple columns or rows,
some screen readers will not correctly link those headers to their associated
data cells — even when header groups (role="rowgroup") are used. This is a
known limitation of the ARIA table model. If your table requires spanning
headers, use a native <table> element so that browsers can apply their
built-in cell-to-header association algorithms.
Disabled
Element type does not support disabled state
| Standard | Criteria |
|---|---|
| WCAG | 4.1.2 Name, Role, Value |
| EN 301 549 | 9.4.1.2 Name, Role, Value |
| Section 508 | 1194.21(d) |
Impact: Serious
Tables are static data structures and do not have a disabled state. Therefore
the disabled attribute is not valid on <table>, <thead>, <tbody>,
<tfoot>, <tr>, or <td> elements. Similarly, aria-disabled carries no
defined meaning for non-interactive roles such as table, rowgroup, row, or
cell. Applying either to these elements communicates a state that the role
does not support, which assistive technologies will handle inconsistently.
Two exceptions apply: Header cells that support user interactions such as
filtering or sorting may use aria-disabled, and interactive tables that
implement the data grid
pattern (role="grid") support aria-disabled on the grid itself and its
cells.
Cell index
When some rows or columns of a table are not always present in the DOM, it may be necessary to index rows or columns so that browsers may properly index them in context of the overall table and convey that information to assistive technologies. The tests in this section check for problems with that indexing.
Row index with no row count context (aria-rowindex)
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
aria-rowindex is only meaningful when aria-rowcount is also set on the table
element. Without aria-rowcount, assistive technologies cannot determine the
full size of the table, making the index value meaningless to users.
Column index with no column count context (aria-colindex)
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
aria-colindex is only meaningful when aria-colcount is also present on the
table element. Set aria-colcount to the total number of columns in the
complete table, including any columns not currently rendered.
Incorrect aria-rowcount value
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Moderate
The value of aria-rowcount must not be less than the number of rows currently
present in the DOM. Because aria-rowcount declares the total row count of the
complete (possibly virtualized) table, a value smaller than the number of
already-rendered rows is a clear contradiction that causes screen readers to
misreport the table’s dimensions.
Incorrect aria-colcount value
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Moderate
The value of aria-colcount must not be less than the number of cells currently
present in the DOM. Because aria-colcount declares the total column count of
the complete table, a value smaller than the number of already-rendered cells is
a clear contradiction that causes screen readers to misreport the table’s width.
Negative row or column index
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
aria-rowindex and aria-colindex must be positive integers greater than or
equal to 1. Negative values and zero are invalid and will be ignored or
misinterpreted by assistive technologies.
Index out of range
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
aria-rowindex must not exceed the value of aria-rowcount, and
aria-colindex must not exceed aria-colcount. Out-of-range values indicate a
mismatch between the rendered cells and the declared table dimensions, which
screen readers will surface as confusing position announcements.
Inconsistent index values
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
aria-rowindex and aria-colindex values must be strictly ascending with no
duplicates across the rendered rows or cells. Gaps from the table start are
valid and expected in virtualized tables — for example, a table rendering rows
50–75 of 500 should use index values 50–75, not 1–26. Inconsistent values make
it impossible for assistive technologies to correctly determine a cell’s
position within the complete table.
Mixed aria-colindex implementation
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Minor
aria-colindex must be applied at a consistent level: either on all row
elements (<tr> / role="row") or on all individual cells, but not on both
rows and cells simultaneously. Mixing the two levels produces conflicting
column-position information that assistive technologies cannot resolve reliably.
Missing aria-colindex
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Serious
When aria-colcount is defined on the table, every row or every cell must also
have aria-colindex set — the same level used consistently throughout the
table. Without it, screen readers cannot determine which column each row or cell
occupies within the complete table.
Relationships
A table’s accessibility depends not only on its internal structure but also on how it relates to surrounding elements. Keeping the table’s DOM hierarchy intact and well-formed is the most reliable foundation for correct assistive technology behavior.
Internal and external relationships that affect accessibility are checked in the following tests.
Nested tables
Unsupported table structure
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
Nesting tables within table cells is not recommended. Although most assistive technologies can parse nested tables, this structure results in inner tables being treated as separate tables by screen readers, which can make navigation confusing and increase cognitive load for users navigating with keyboard shortcuts or landmarks.
Nested tables are not automatically included in accessibility evaluations by Evinced’s component analysis tools.
Aria Owns
Set on an element with role="table" to identify elements with role row or
rowgroup when they and their child columnheader, rowheader, or cell
elements cannot be included in the table’s DOM hierarchy.
Invalid aria-owns values
Best practice.
Impact: Varies
Elements referenced by the aria-owns attribute on a table container (<table>
or role="table") have roles that do not conform to the expected child roles
for a table. A table container should only own elements with role="row" or
role="rowgroup". When referenced elements carry incompatible roles, screen
readers may misinterpret the table's structure, which can confuse users
navigating the data.
Avoid aria-owns
Best practice.
Impact: Varies
The aria-owns attribute is poorly supported for table structures. Avoid using
aria-owns if possible.
Assistive technology support is poor and it can produce an inconsistent reading order. If rows or cells are currently rendered outside the table container, restructure the component so they are direct descendants of the table element, or switch to a virtualization library that supports in-tree row rendering rather than “portaling” rows into a separate DOM node outside of the component hierarchy.
If you must use it:
- Preserve DOM order: DOM children of the container should come before
explicitly owned elements in the DOM order unless you explicitly list the DOM
children in
aria-ownsin the desired reading order. - Match expected roles: All elements specified in
aria-ownsmust have roles that are allowed to be children of the container. - Avoid duplicate or inconsistent references: An element’s
idmust not appear in more than one element’saria-ownsattribute at any time.
Operation
Tables are typically read-only and do not require interaction. In tables that support column sorting or row selection, keyboard interactions must be implemented with care to ensure all users can operate them effectively.
Accessible table operability is checked with the following tests.
Aria Sort
aria-sort invalid value
| Standard | Criteria |
|---|---|
| WCAG | 4.1.2 Name, Role, Value |
| EN 301 549 | 9.4.1.2 Name, Role, Value |
| Section 508 | 1194.21(d) |
Impact: Minor
The aria-sort attribute must be set to one of the four valid values:
ascending, descending, none, or other. An unrecognized value is ignored
by assistive technologies, leaving users without sort-direction feedback for
that column.
aria-sort on a table cell
| Standard | Criteria |
|---|---|
| WCAG | N/A |
| EN 301 549 | N/A |
| Section 508 | N/A |
Impact: Minor
The aria-sort attribute is only meaningful on header cells — <th> elements
for native tables, or elements with role="columnheader" or role="rowheader"
for ARIA tables. Placing it on a data cell or other element has no effect and
may produce unexpected behavior in assistive technologies.
Sort not accessible to keyboard
| Standard | Criteria |
|---|---|
| WCAG | 2.1.1 Keyboard |
| EN 301 549 | 9.2.1.1 Keyboard |
| Section 508 | 1194.21(a) |
Impact: Serious
A column header that triggers sorting has been detected, but it cannot be
reached or activated by keyboard. All interactive sort controls must be
reachable via the Tab key and can be activated with Enter or Space, so
that keyboard-only users have the same sorting capability as pointer users.
Ambiguous multi-sort levels
| Standard | Criteria |
|---|---|
| WCAG | 1.3.1 Info and Relationships |
| EN 301 549 | 9.1.3.1 Info and Relationships |
| Section 508 | N/A |
Impact: Minor
When a table supports multi-column sorting, only the column with the primary
sort priority should have aria-sort set to ascending, descending, or
other. All other participating columns must have aria-sort="none" or have
the attribute removed. Setting an active sort direction on more than one column
simultaneously makes the sort order ambiguous for screen reader users, who
cannot determine which column takes precedence.