Skip to content
Back to skills

Tables

ASecurity

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.

  • 47 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added May 26, 2026
designawstestinggit

Works with

  • cli

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add mgifford/accessibility-skills --skill tables --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tables?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tables
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mgifford-tables/badge)](https://www.skillsdirectory.com/skills/mgifford-tables)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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/>

Files in this skill

  • README.md573 B
  • SKILL.md12.4 KB
  • SYNC.md911 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.