# Data Tables and Accessibility Markup

> Canonical HTML version: https://rocketvalidator.com/glossary/data-tables-accessibility-markup
> Attribution: Rocket Validator (https://rocketvalidator.com)
> License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

Data tables require specific HTML markup to be accessible, including proper use of `<th>`, `<caption>`, `scope`, and `headers` attributes so that screen readers can associate data cells with their corresponding row and column headers.

HTML tables used to present data need structured markup that goes beyond visual formatting. When a sighted user scans a table, they can glance at column and row headers to understand what each cell means. Screen reader users don't have that luxury. They rely on programmatic associations between header cells (`<th>`) and data cells (`<td>`) to navigate and interpret table content. Without these associations, a screen reader reads cells in sequence with no context, turning structured data into a stream of disconnected values.

Accessible table markup involves several HTML elements and attributes working together: `<caption>` provides a visible or programmatically available title for the table, `<th>` identifies header cells, the `scope` attribute clarifies whether a header applies to a column or row, and the `headers` attribute creates explicit associations in complex tables where `scope` alone is insufficient. Getting these right is both a WCAG requirement (primarily Success Criterion 1.3.1, Info and Relationships) and an HTML validation concern, since misusing table elements triggers warnings and errors in validators.

## Why data tables and accessibility markup matters

Screen reader users navigate tables cell by cell using keyboard shortcuts. As they move through a table, the screen reader announces the associated header for each data cell. If a table lacks `<th>` elements or `scope` attributes, the screen reader cannot make these announcements, and the user hears raw values like "42" or "Yes" with no indication of what column or row those values belong to.

This affects blind and low vision users directly, but it also affects users of other assistive technologies and anyone who relies on the accessibility tree for information. Search engines and content extraction tools also benefit from properly structured tables.

Common problems include using `<td>` for everything (including headers), relying on visual styling like bold text to indicate headers instead of semantic markup, omitting `<caption>`, and using layout tables where CSS layout would be more appropriate.

## How data tables and accessibility markup works

### Simple tables with `scope`

For tables with a single row of column headers or a single column of row headers, the `scope` attribute on `<th>` is enough to create clear associations. Setting `scope="col"` tells assistive technology that the header applies to all cells in that column. Setting `scope="row"` does the same for rows.

The `<caption>` element should appear as the first child of `<table>`. It gives the table a name, which screen readers announce when the user encounters the table, helping them decide whether to explore it.

### Complex tables with `headers`

When a table has merged cells (using `colspan` or `rowspan`), multiple levels of headers, or an irregular structure, `scope` can become ambiguous. In these cases, assign an `id` to each `<th>` and use the `headers` attribute on each `<td>` to list the `id` values of all headers that apply to that cell, separated by spaces.

This approach is more verbose but gives screen readers an unambiguous mapping from every data cell to its headers.

### Layout tables vs. data tables

Tables used purely for visual layout should not have `<th>`, `<caption>`, or `summary` attributes. Adding `role="presentation"` or `role="none"` to a layout table tells assistive technology to ignore the table semantics. However, CSS-based layouts are strongly preferred over layout tables.

## Code examples

### Bad example: table with no header markup

```html
<table>
  <tr>
    <td>Name</td>
    <td>Department</td>
    <td>Extension</td>
  </tr>
  <tr>
    <td>Ava Chen</td>
    <td>Engineering</td>
    <td>4012</td>
  </tr>
  <tr>
    <td>Marcus Hall</td>
    <td>Design</td>
    <td>4037</td>
  </tr>
</table>
```

A screen reader reading this table has no way to know that "Name", "Department", and "Extension" are headers. When the user reaches the cell "4037", the reader announces "4037" with no context.

### Good example: simple table with `<caption>` and `scope`

```html
<table>
  <caption>Staff directory</caption>
  <thead>
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Department</th>
      <th scope="col">Extension</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ava Chen</td>
      <td>Engineering</td>
      <td>4012</td>
    </tr>
    <tr>
      <td>Marcus Hall</td>
      <td>Design</td>
      <td>4037</td>
    </tr>
  </tbody>
</table>
```

Now the screen reader can announce "Extension: 4037" when the user navigates to that cell. The `<caption>` lets the user know the table's purpose before they start exploring it.

### Good example: complex table with `headers`

```html
<table>
  <caption>Quarterly sales by region</caption>
  <thead>
    <tr>
      <td></td>
      <th id="q1" scope="col">Q1</th>
      <th id="q2" scope="col">Q2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th id="north" scope="row">North</th>
      <td headers="north q1">$120k</td>
      <td headers="north q2">$135k</td>
    </tr>
    <tr>
      <th id="south" scope="row">South</th>
      <td headers="south q1">$98k</td>
      <td headers="south q2">$110k</td>
    </tr>
  </tbody>
</table>
```

Each `<td>` explicitly lists the `id` values of its column header and row header. A screen reader navigating to "$135k" can announce "North, Q2: $135k." In a simple two-axis table like this one, `scope` alone would suffice, but the `headers` attribute is shown here to illustrate the technique for cases where merged cells or multi-level headers make `scope` ambiguous.

### Bad example: layout table with misleading semantics

```html
<table>
  <caption>Page layout</caption>
  <tr>
    <th>Sidebar</th>
    <th>Main content</th>
  </tr>
  <tr>
    <td>Navigation links here</td>
    <td>Article body here</td>
  </tr>
</table>
```

This table is used for layout, not data. The `<caption>` and `<th>` elements cause screen readers to treat it as a data table and announce header associations that are meaningless. If a table must be used for layout, strip all data table semantics:

```html
<table role="presentation">
  <tr>
    <td>Navigation links here</td>
    <td>Article body here</td>
  </tr>
</table>
```

Better still, replace the layout table with CSS Grid or Flexbox.
