$npx -y skills add mgifford/accessibility-skills --skill tablesLoad 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 head
| 1 | # Tables Accessibility Skill |
| 2 | |
| 3 | > **Canonical source**: `examples/TABLES_ACCESSIBILITY_BEST_PRACTICES.md` in `mgifford/ACCESSIBILITY.md` |
| 4 | > This skill is derived from that file. When in doubt, the example is authoritative. |
| 5 | |
| 6 | Apply these rules when creating or reviewing HTML data tables. |
| 7 | **Only load this skill if the project contains data tables.** |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Core Mandate |
| 12 | |
| 13 | Use tables for information whose meaning depends on relationships between |
| 14 | rows and columns. Mark those relationships in HTML so users can identify the |
| 15 | table, navigate its cells, and hear the relevant headers with each value. The |
| 16 | complexity of the markup should match the complexity of the information — |
| 17 | simplifying or splitting a table is often better than adding increasingly |
| 18 | complicated header associations. |
| 19 | |
| 20 | **Never use tables for layout.** Use CSS Grid/Flexbox instead. Do not turn a |
| 21 | reading-oriented data table into an application-style grid (`role="grid"`) |
| 22 | unless users genuinely need spreadsheet-like keyboard interaction. |
| 23 | |
| 24 | --- |
| 25 | |
| 26 | ## Severity Scale (this skill) |
| 27 | |
| 28 | | Level | Meaning | |
| 29 | |---|---| |
| 30 | | **Critical** | Table data is completely uninterpretable by screen reader users | |
| 31 | | **Serious** | Headers missing or misassociated; complex table with no navigation aid | |
| 32 | | **Moderate** | No name for the table when multiple tables share a page; zebra stripes lost in forced-colours without fallback | |
| 33 | | **Minor** | `<thead>`/`<tbody>` absent; obsolete `summary` attribute used | |
| 34 | |
| 35 | --- |
| 36 | |
| 37 | ## Choose the Correct Structure First |
| 38 | |
| 39 | * A **list** — items that don't depend on column relationships |
| 40 | * A **definition list** — term/description pairs |
| 41 | * A **data table** — reading and comparing row-column data |
| 42 | * An **ARIA grid/treegrid** — only for an application-style composite widget |
| 43 | with cell navigation or editing |
| 44 | * **CSS Grid/Flexbox** — page layout |
| 45 | |
| 46 | Do not use a table merely to align form fields, cards, images, or page regions. |
| 47 | |
| 48 | --- |
| 49 | |
| 50 | ## Critical: Every Data Table Must Have `<th>` with `scope` |
| 51 | |
| 52 | A table with only `<td>` cells is **Critical** — screen readers announce raw |
| 53 | data with no context. Use `<th>` for cells that label a row, column, or |
| 54 | group — never a bolded `<td>` as a visual substitute. |
| 55 | |
| 56 | ```html |
| 57 | <table> |
| 58 | <caption>Monthly sales by region, first quarter 2025</caption> |
| 59 | <thead> |
| 60 | <tr> |
| 61 | <th scope="col">Region</th> |
| 62 | <th scope="col">January</th> |
| 63 | <th scope="col">February</th> |
| 64 | </tr> |
| 65 | </thead> |
| 66 | <tbody> |
| 67 | <tr> |
| 68 | <th scope="row">North</th> |
| 69 | <td>$12,400</td> |
| 70 | <td>$14,200</td> |
| 71 | </tr> |
| 72 | </tbody> |
| 73 | </table> |
| 74 | ``` |
| 75 | |
| 76 | For a small, unambiguous table with only one header row or column, browsers |
| 77 | can sometimes infer header direction from `<th>` alone. Explicit |
| 78 | `scope="col"`/`scope="row"` remains the dependable authoring convention, |
| 79 | especially for large or similar-looking data. `<thead>`/`<tbody>`/`<tfoot>` |
| 80 | support sticky headers, styling, scripting, and repeating headers in print — |
| 81 | their presence does not by itself repair missing or incorrect header associations. |
| 82 | |
| 83 | When a header row has an empty top-left corner cell (no header function), |
| 84 | leave it as `<td>` — don't add filler text like "blank" just to occupy the cell. |
| 85 | |
| 86 | --- |
| 87 | |
| 88 | ## Moderate: Captions and Table Names |
| 89 | |
| 90 | `<caption>` is the native way to identify a table, especially with several |
| 91 | tables on one page. **WCAG does not require a caption in every case** — an |
| 92 | immediately associated heading or surrounding text can sometimes adequately |
| 93 | identify a simple table. Treat `<caption>` as the preferred default, but |
| 94 | don't force a duplicate name when another tested association already |
| 95 | supplies one. |
| 96 | |
| 97 | ```html |
| 98 | <table> |
| 99 | <caption>2025 budget allocation by department</caption> |
| 100 | <!-- header and data rows --> |
| 101 | </table> |
| 102 | ``` |
| 103 | |
| 104 | The caption must be a direct child of `<table>` (before `<thead>`), concise |
| 105 | and specific. For unusual structure, a short summary can explain row/column |
| 106 | organization without repeating every value: |
| 107 | |
| 108 | ```html |
| 109 | <caption> |
| 110 | Availability of holiday accommodation |
| 111 | <span class="table-summary">Locations form row groups. Property types form columns.</span> |
| 112 | </caption> |
| 113 | ``` |
| 114 | |
| 115 | The obsolete HTML `summary` attribute on `<table>` should not be used — use |
| 116 | visible caption content or a nearby description instead. |
| 117 | |
| 118 | --- |
| 119 | |
| 120 | ## Serious: Spanned Headers — `scope="colgroup"` / `scope="rowgroup"` |
| 121 | |
| 122 | Use `colgroup`/`rowgroup` scope values **only when the corresponding group |
| 123 | actually exists** in the table structure (matching `<colgroup>` or `<tbody>` |
| 124 | row group): |
| 125 | |
| 126 | ```html |
| 127 | <table> |
| 128 | <caption>Quarterly revenue by product line, CAD thousands</caption> |
| 129 | <colgroup><col></colgroup> |
| 130 | <colgroup span="2"></colgroup> |
| 131 | <colgroup span="2"> |