( Perceivable / WCAG 1.3.1 )
Complex data table is missing a caption
What is this issue?
A <table> element that holds real data (multiple rows, multiple columns, more than a trivial amount of content) has no <caption> element as its first child, and no other reliable naming mechanism (like aria-label) gives it an accessible name either. The table’s headers might be fine and its structure might be otherwise valid; it simply has no title of its own.
<caption> is the element HTML defines specifically for this purpose: a short description of the table that a screen reader announces before reading any of its content, functioning as the table’s accessible name. Nearby body text or a visually adjacent heading isn’t the same thing, because neither is programmatically tied to the table itself.
Why does this matter?
A sighted user scanning a page with several tables can usually tell them apart at a glance: different column headers, different surrounding context, different visual position. A screen reader user who navigates directly to a table using a table-navigation shortcut gets none of that visual context; the first thing they hear is whatever the table announces about itself, which, with no caption, is often just “table” and a row/column count.
On a page with a single simple table next to a paragraph that already describes it, the loss is minor. On a page with several data tables (quarterly figures for different regions, say), the loss compounds: a user jumping between tables to compare them has no way to confirm which one they’ve landed on without backing out and reading surrounding text every single time, turning what should be instant table-to-table comparison into a much slower, uncertain process.
Who is affected?
- Screen reader users: land on an unnamed table with no way to confirm what it represents until they’ve read into its contents, and lose the ability to distinguish between multiple tables on the same page without extra navigation.
- Cognitive disabilities: users who rely on a title to decide whether a table is relevant before committing to reading through its data have to read the whole table first to make that same judgment.
What users experience
Sana uses VoiceOver on her Mac to compare two quarterly reports on a company’s investor relations page, using the Rotor to jump directly between tables. Neither table has a caption, so VoiceOver announces both simply as “table, 5 rows, 3 columns” with no distinguishing title. Sana can’t tell which table covers Q1 and which covers Q2 without reading into the first row of actual data in each one, so what should have been an instant jump-and-compare between two tables turns into reading both tables from the start, every time she wants to confirm which is which.
How do I fix it?
Add a <caption> element as the first child inside the <table>, stating clearly what the table represents: subject and, where relevant, a time period or scope. This works because <caption> is announced by screen readers before any of the table’s rows, giving the user a title to orient by, and it does this regardless of how they arrived at the table, whether by reading the page in order or jumping directly to it with a navigation shortcut.
Write the caption the way you’d title a chart: specific enough to distinguish it from any other table on the same page, without repeating the individual column headers underneath it. If a heading immediately precedes the table in the visual layout, a caption isn’t redundant with it: the heading only helps a user reading in document order, while the caption helps every user regardless of navigation method, so both can and often should exist together.
Code Examples
<table>
<tr><th scope="col">Month</th><th scope="col">Revenue</th></tr>
<tr><td>April</td><td>$48k</td></tr>
</table>
<table>
<tr><th scope="col">Month</th><th scope="col">Expenses</th></tr>
<tr><td>April</td><td>$31k</td></tr>
</table><table>
<caption>Monthly revenue, 2026</caption>
<tr><th scope="col">Month</th><th scope="col">Revenue</th></tr>
<tr><td>April</td><td>$48k</td></tr>
</table>
<table>
<caption>Monthly expenses, 2026</caption>
<tr><th scope="col">Month</th><th scope="col">Expenses</th></tr>
<tr><td>April</td><td>$31k</td></tr>
</table>Adding one <caption> per table gives each one a distinct, announced title, “Monthly revenue, 2026” versus “Monthly expenses, 2026”, so a screen reader user landing directly on either table, in any order, immediately knows which is which without reading a single data row.
Common Mistakes
Mistake: “The page heading right above the table already says what it is, so a caption would be redundant.” A heading only helps a user who reads the page from top to bottom in order; it provides no context to someone who jumps straight into the table using a table-navigation shortcut, which skips past surrounding headings entirely. A caption is announced as part of the table itself, so it reaches every user regardless of how they got there.
Mistake: “I’ll put the table’s title in a p tag right above it instead of using caption.” A <p> tag placed visually above a table has no programmatic connection to it: a screen reader treats it as unrelated body text, not the table’s name, and it isn’t announced when the user navigates directly into the table. Only <caption> (or aria-label on the <table> element) is actually tied to the table as its accessible name.
Mistake: “This table is simple enough, just two columns, so it doesn’t need a caption.” Column count alone isn’t the deciding factor; what matters is whether the table’s identity is clear to someone landing on it directly, with no other context. A two-column table is a strong candidate to skip a caption only when it sits alone on the page next to text that already names it unambiguously; on a page with more than one table, even a simple one benefits from its own title.
How RedFlag Detects This
Guidance only: RedFlag documents this issue but does not currently flag it; verify manually. Judging whether a specific table is complex enough to need its own caption, and whether nearby text already provides equivalent identification, requires contextual judgment that RedFlag’s automated and custom detectors do not attempt today. This rule id is not wired into RedFlag’s detection engine or coverage model in any form.
False negative: every occurrence is effectively a false negative in the sense that nothing is ever automatically flagged; a page full of uncaptioned tables passes every automated RedFlag scan silently. False positive: not applicable, since this check never raises an automated violation to begin with. Manual step: review every data table on the page, particularly any page with more than one table, and confirm each has a <caption> (or aria-label) that would let a screen reader user identify it immediately upon arrival, without reading into its rows.
Manual Testing
- Open the page in Chrome or Firefox with NVDA or JAWS running on Windows, or VoiceOver on macOS.
- Navigate directly to each table using table-navigation commands (NVDA/JAWS: T to jump table to table in browse mode; VoiceOver: Rotor set to Tables) rather than reading the page in order.
- Listen to what’s announced immediately upon landing on the table; it should include a specific, descriptive title, not just “table” with a row/column count.
- If the page has more than one table, confirm each announces a distinct title that lets you tell them apart without reading any data rows.
- Inspect the markup in browser dev tools to confirm a
<caption>element (oraria-labelon the<table>) is actually present, since a nearby heading or paragraph won’t satisfy this check even if it visually reads as a title.
Related WCAG Success Criteria
1.3.1 Info and Relationships: Information, structure, and relationships conveyed through presentation must also be available programmatically. A table’s identity (what it represents, distinct from any other table on the page) is exactly this kind of relationship: obvious from surrounding visual context to a sighted user, but only available programmatically through an actual <caption> or accessible name.
Related Issues
Table is missing header cells covers the more fundamental version of this same “orientation” problem: no headers means a screen reader user gets no context inside the table, on top of no caption meaning they get no context arriving at it.
Table cell headers attribute references a missing id and Table header has no associated data cells commonly appear on the same genuinely complex tables that most need a caption, since all three checks target the kind of multi-header data table where identification and per-cell context both matter most.
Table header scope value is invalid is another header-association failure that tends to cluster on the same complex tables, worth checking together whenever one of them turns up in an audit.
References
- W3C Understanding 1.3.1: Info and Relationships
- MDN: The caption element
- WebAIM: Creating Accessible Tables
Frequently asked questions
Does every table on a page need a caption, or just complex ones?
A short, simple table next to explanatory text can sometimes get by without one, but any table with more than a handful of rows, multiple headers, or no other nearby heading identifying it should have a caption. When in doubt, add one: a caption rarely hurts and often helps.
Is a heading placed directly above a table a substitute for a caption?
It can work if a screen reader user is reading the page in order and encounters the heading right before the table, but it does not work if they land on the table directly through table-navigation commands, which skip past headings entirely. A caption is announced as part of the table itself, so it works regardless of how the user arrived there.
Can I use aria-label on the table element instead of a caption?
Yes, aria-label provides an accessible name for the table in a similar way, and some developers prefer it when a visible caption does not fit the design. A visible caption is still generally preferred because it also benefits sighted users scanning the page, which aria-label does not.
Does a caption need to repeat information already in the table headers?
No. A caption should describe what the table as a whole represents, such as a title or a summary of the time period and subject, not restate individual column headers. Repeating header text in the caption adds redundancy without adding new context.
Why doesn't RedFlag flag missing table captions automatically?
Judging whether a specific table is complex enough to need a caption, and whether nearby text already provides equivalent identification, requires contextual judgment RedFlag current automated and custom detectors do not attempt. This check is documented for manual review rather than flagged automatically.