Load this skill whenever the project contains HTML data tables (<table> elements). Under no circumstances use tables for layout purposes. Absolutely always include <th> elements with appropriate scope attributes, a <caption> or aria-labelledby, and ensure complex tables have headers associated with data cells. Apply these rules to every data table without exception.
Install via CLI
openskills install mgifford/accessibility-skills---
name: tables
description: >
Load this skill whenever the project contains HTML data tables (<table>
elements). Under no circumstances use tables for layout purposes. Absolutely
always include <th> elements with appropriate scope attributes, a <caption>
or aria-labelledby, and ensure complex tables have headers associated with
data cells. Apply these rules to every data table without exception.
---
# Tables Accessibility Skill
> **Canonical source**: `examples/TABLES_ACCESSIBILITY_BEST_PRACTICES.md` in `mgifford/ACCESSIBILITY.md`
> This skill is derived from that file. When in doubt, the example is authoritative.
Apply these rules when creating or reviewing HTML data tables.
**Only load this skill if the project contains data tables.**
---
## Core Mandate
Tables communicate relationships between data. Sighted users scan rows and columns
visually; screen reader users navigate cell by cell and rely on header announcements
for context. Without proper markup, every cell is an orphaned data point.
**Never use tables for layout.** Use CSS (Grid, Flexbox) instead.
Layout tables that remain in a codebase must have `role="presentation"` and
must linearise without loss of meaning.
---
## Severity Scale (this skill)
| Level | Meaning |
|---|---|
| **Critical** | Table data is completely uninterpretable by screen reader users |
| **Serious** | Headers missing or misassociated; complex table with no navigation aid |
| **Moderate** | `<caption>` missing; zebra stripes lost in forced-colours without fallback |
| **Minor** | `<thead>`/`<tbody>` absent; `summary` attribute used (deprecated) |
---
## Assistive Technology Context
Tables are rendered differently across AT combinations. Test with:
| AT | Browser | Notes |
|---|---|---|
| NVDA | Chrome | Reads headers on cell entry; table navigation via `T` key |
| JAWS | Chrome | Similar to NVDA; `Ctrl+Alt+Arrow` navigates cells |
| VoiceOver | Safari (macOS) | `VO+Arrow` navigates; announces scope-based headers reliably |
| VoiceOver | Safari (iOS) | Swipe navigation; complex tables challenging |
| TalkBack | Chrome (Android) | Linear reading; simpler tables work better |
| Voice Control | Any | Navigation by table elements less common; focus on operability |
| Screen magnification | Any | Wide tables require horizontal scroll — use responsive patterns |
| Reader Mode | Firefox/Edge/Safari | Strips CSS; table structure must be semantically meaningful |
| Edge Read Aloud | Edge | Reads table content linearly; `<caption>` helps orientation |
**Reader Mode note:** Firefox, Safari, and Edge all have reader modes that
reformat page content. Tables survive reader mode best when `<caption>` is
present and structure is simple. Complex multi-level tables may be stripped
or reflowed unexpectedly — provide a text summary for high-complexity tables.
---
## Critical: Every Data Table Must Have `<th>` with `scope`
A table with only `<td>` cells is **Critical** — screen readers announce raw
data with no context about what each cell means.
**All `<th>` elements must have a `scope` attribute.** While screen readers
may infer `col` or `row` from layout, explicit `scope` is unambiguous and
required for reliable AT support.
### Simple table — one set of column headers
```html
<table>
<caption>Monthly sales by region, Q1 2024</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>$14,200</td>
<td>$16,800</td>
</tr>
<tr>
<th scope="row">South</th>
<td>$9,100</td>
<td>$10,300</td>
<td>$11,900</td>
</tr>
</tbody>
</table>
```
`scope="col"` on column headers; `scope="row"` on row headers.
When both are present, the screen reader announces both before the data cell.
---
## Critical: `<caption>` on Every Data Table
A table without `<caption>` is **Moderate** in isolation but **Serious** when
multiple tables appear on one page — users cannot distinguish which table they
are in. Make `<caption>` a universal requirement.
```html
<table>
<caption>2024 budget allocation by department</caption>
…
</table>
```
`<caption>` is the first child of `<table>` — before `<thead>`. It is
announced by screen readers when the user enters the table. It also helps
users in Reader Mode identify the table's purpose after CSS is stripped.
---
## Serious: Spanned Headers — `scope="colgroup"` / `scope="rowgroup"`
When a header spans multiple columns or rows, use `colgroup` or `rowgroup`
scope values:
```html
<table>
<caption>Quarterly revenue by product line (USD thousands)</caption>
<thead>
<tr>
<th scope="col" rowspan="2">Product</th>
<th scope="colgroup" colspan="2">H1</th>
<th scope="colgroup" colspan="2">H2</th>
</tr>
<tr>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
<th scope="col">Q4</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Hardware</th>
<td>1,200</td>
<td>1,450</td>
<td>1,100</td>
<td>1,800</td>
</tr>
</tbody>
</table>
```
**Test spanned tables with NVDA+Chrome and JAWS+Chrome** — some screen readers
still handle complex spanned headers inconsistently. If testing reveals problems,
simplify the table structure before reaching for `headers`/`id`.
---
## Serious: When to Use `headers` + `id` (Rarely)
The `headers` and `id` approach associates individual cells with specific
header cells by ID reference. Use it **only** for tables so complex that
`scope` causes headers to apply to the wrong cells.
```html
<table>
<caption>Staff schedules by shift and department</caption>
<tr>
<th id="dept">Department</th>
<th id="am">AM shift</th>
<th id="pm">PM shift</th>
</tr>
<tr>
<th id="nursing">Nursing</th>
<td headers="nursing am">8</td>
<td headers="nursing pm">6</td>
</tr>
</table>
```
**Caution:** Even when `headers`/`id` is technically correct, a table complex
enough to need it may be functionally inaccessible — reading three or four
headers before each cell is confusing in practice. Prefer simplifying the table.
Per WebAIM: "If there are multiple levels of row and/or column headers being
read, it will not likely be functionally accessible or understandable to a
screen reader user."
---
## Moderate: `<thead>`, `<tbody>`, `<tfoot>`
These semantic elements have no direct AT benefit on their own, but `<thead>`
enables `display: table-header-group` in print CSS, repeating column headers
on every printed page — important for multi-page tables.
```html
<table>
<caption>…</caption>
<thead>
<tr><th scope="col">…</th></tr>
</thead>
<tbody>
<tr><td>…</td></tr>
</tbody>
<tfoot>
<tr><td colspan="4">Total: $45,600</td></tr>
</tfoot>
</table>
```
---
## Moderate: Responsive Tables
Wide tables require horizontal scrolling for low-vision users who zoom.
Always wrap tables in a scrollable container — never clip overflow silently:
```html
<!-- Scrollable container with accessible label -->
<div role="region"
aria-labelledby="table-caption-id"
tabindex="0"
style="overflow-x: auto;">
<table>
<caption id="table-caption-id">Monthly sales by region</caption>
…
</table>
</div>
```
`tabindex="0"` makes the scroll container keyboard-focusable so keyboard users
can scroll it. `role="region"` + `aria-labelledby` announces it as a landmark.
For small screens, consider a card-based alternative layout via CSS:
```css
@media (max-width: 600px) {
table, thead, tbody, th, td, tr { display: block; }
thead tr { position: absolute; top: -9999px; left: -9999px; }
td::before {
content: attr(data-label) ": ";
font-weight: bold;
}
}
```
When using the card pattern, add `data-label` attributes to each `<td>`:
```html
<td data-label="Region">North</td>
<td data-label="January">$12,400</td>
```
---
## Moderate: Sortable Columns
Interactive sortable columns must be keyboard operable and announce state:
```html
<th scope="col">
<button type="button"
aria-sort="ascending"
aria-label="Sort by Region, currently ascending">
Region
<svg aria-hidden="true" focusable="false"><!-- sort icon --></svg>
</button>
</th>
```
`aria-sort` values: `ascending`, `descending`, `none`, `other`.
Place `aria-sort` on the `<th>`, not the `<button>` — or on both if needed
for maximum AT compatibility. Test with NVDA and JAWS; announcement varies.
---
## Moderate: Colour in Tables
Zebra stripes (alternating row backgrounds) are a common pattern that vanishes
in forced-colours mode and when printing with backgrounds off:
```css
@media print {
tbody tr { border-bottom: 1px solid #333; }
}
@media (forced-colors: active) {
tbody tr { border-bottom: 1px solid CanvasText; }
}
```
Never use background colour alone to convey meaning (e.g., red rows = overdue).
Always pair colour with a text label or icon.
---
## Layout Tables — What to Do With Them
Layout tables in legacy codebases must be marked to remove them from table
navigation mode:
```html
<table role="presentation">
<!-- layout content -->
</table>
```
`role="presentation"` removes table semantics from the AT tree. The content
must still linearise logically when read top-to-bottom, left-to-right.
Verify by disabling CSS and reading the page linearly.
---
## CMS and Framework Notes
**Drupal:** The CKEditor rich text editor in Drupal includes a table plugin.
Configure it to require `<caption>` and `scope` attributes via editor configuration.
The [Drupal Accessibility Coding Standards](https://www.drupal.org/docs/getting-started/accessibility/accessibility-coding-standards)
require WCAG 2.1 AA compliance for contributed modules and themes — table
markup in contrib must follow these rules.
**WordPress, other CMS:** Block editors often generate tables without `<caption>`
or `scope`. Audit CMS-generated table markup and configure the editor or post-process
the output to add missing attributes.
**Generated tables (charts/dashboards):** When JS libraries generate tables
from data (e.g., DataTables.js, AG Grid), verify the library outputs `<th scope="col">`,
`<caption>`, and `<thead>`. Many do not by default — check configuration options.
---
## Definition of Done Checklist
* [ ] No tables used for layout — CSS used instead; existing layout tables have `role="presentation"`
* [ ] Every data table has a `<caption>` as its first child
* [ ] All `<th>` elements have explicit `scope` attribute (`col`, `row`, `colgroup`, or `rowgroup`)
* [ ] `<thead>` present; `<tbody>` present
* [ ] Spanned headers use `colgroup`/`rowgroup` scope values
* [ ] `headers`/`id` used only where `scope` is genuinely insufficient
* [ ] Wide tables wrapped in `role="region"` + `aria-labelledby` + `tabindex="0"` scrollable container
* [ ] Sortable columns use `aria-sort` on `<th>`; sort controls keyboard-operable
* [ ] Colour-only row distinction has print + forced-colours fallback
* [ ] `data-label` attributes added for responsive card layout
* [ ] Tested: NVDA+Chrome, JAWS+Chrome, VoiceOver+Safari
* [ ] Tested: Reader Mode (Firefox or Safari) — structure remains meaningful
---
## Key WCAG Criteria
* 1.3.1 Info and Relationships (A) — **Critical if headers absent**
* 1.3.2 Meaningful Sequence (A) — table must linearise logically
* 1.4.1 Use of Color (A) — colour not sole encoding in table cells
* 2.1.1 Keyboard (A) — sortable columns keyboard operable
---
## References
* [Full best practices guide](https://github.com/mgifford/ACCESSIBILITY.md/blob/main/examples/TABLES_ACCESSIBILITY_BEST_PRACTICES.md)
* [Curated resources — tables](../resources/by-topic/tables.md)
* [WAI Tables Tutorial](https://www.w3.org/WAI/tutorials/tables/) — authoritative; covers one-header through multi-level
* [WebAIM: Creating Accessible Tables](https://webaim.org/techniques/tables/data) — scope vs headers/id guidance
* [MDN: HTML table accessibility](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Structuring_content/Table_accessibility)
* [Drupal Accessibility Coding Standards](https://www.drupal.org/docs/getting-started/accessibility/accessibility-coding-standards)
* [WCAG 2.2 Understanding 1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html)
> **Standards horizon:** These rules target WCAG 2.2 AA. No significant changes
> to table accessibility requirements are anticipated in WCAG 3.0.
> Monitor: <https://www.w3.org/TR/wcag-3.0/>
No comments yet. Be the first to comment!