# Data tables include semantic markup

- **Pattern:** `ab-000073` (`accessibility-basics.forms-interactive.data-table-markup`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000073
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000073)

## Why it matters

A data table with `<td>` elements in the header row instead of `<th>` elements loses its entire semantic structure for screen readers. Without `<th scope="col">` headers, screen readers announce cell content without any column or row context — a user navigating a pricing comparison table hears '49, 99, 199' with no indication of which tier each price belongs to. WCAG 2.2 SC 1.3.1 (Info and Relationships) requires that relationships conveyed visually (like column header to data cell) be available programmatically. Section 508 2018 Refresh 502.3.5 covers information and relationships. Complex tables with both row and column headers require `scope="col"`, `scope="row"`, and in multi-header scenarios, `headers` attributes on data cells referencing multiple `id` values.

## Severity rationale

Low because missing table headers create context loss for screen reader users in tabular data without blocking access to the underlying cell values.

## Remediation

Replace `<td>` header rows with `<th>` elements carrying `scope` attributes. Add a `<caption>` describing the table's content. In React/TSX components that render tables:

```tsx
<table>
  <caption>Monthly subscription pricing by plan</caption>
  <thead>
    <tr>
      <th scope="col">Plan</th>
      <th scope="col">Price / month</th>
      <th scope="col">Storage</th>
      <th scope="col">Users</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Starter</th>  {/* Row header */}
      <td>$9</td>
      <td>10 GB</td>
      <td>3</td>
    </tr>
    <tr>
      <th scope="row">Pro</th>
      <td>$29</td>
      <td>100 GB</td>
      <td>Unlimited</td>
    </tr>
  </tbody>
</table>
```

For complex tables where a cell is described by both a column and a row header, use `id` on each header and `headers` on the data cell:

```html
<th id="q1" scope="col">Q1</th>
<th id="revenue" scope="row">Revenue</th>
<td headers="q1 revenue">$120,000</td>
```

Search for `<table>` elements in your component library. Ensure data tables (`src/components/ui/table.tsx` or similar) emit `<th>` with `scope` in `<thead>` rows.

## Detection

- **ID:** `data-table-markup`
- **Severity:** `low`
- **What to look for:** Enumerate every relevant item. Check for `<table>` elements in the application. Tables should include `<thead>` for headers, `<tbody>` for body rows, and `scope="col"` or `scope="row"` on header cells. Caption or visible title should describe the table. Complex tables should have a summary.
- **Pass criteria:** Tables use proper semantic markup with `<thead>`, `<tbody>`, and `<tfoot>` (if present). Header cells have `scope` attribute. Tables have a caption or title.
- **Fail criteria:** Tables use plain `<td>` for headers. No header scope attributes. No caption or title.
- **Skip (N/A) when:** The application has no data tables (layout tables are exempt, though CSS layout is preferred).
- **Detail on fail:** Describe table markup issues. Example: `"Results table uses <td> for header row instead of <th>. No scope attributes on headers. No caption describing the table."`
- **Remediation:** Use semantic table markup:

  ```tsx
  <table>
    <caption>2024 Sales by Quarter</caption>
    <thead>
      <tr>
        <th scope="col">Quarter</th>
        <th scope="col">Revenue</th>
        <th scope="col">Growth</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Q1</th>
        <td>$100K</td>
        <td>5%</td>
      </tr>
    </tbody>
  </table>
  ```

## External references

- wcag 1.3.1
- section-508 502.3.5

Taxons: accessibility

HTML version: https://auditbuffet.com/patterns/ab-000073
