Tables Rule

Table captions must use the caption element table-fake-caption

A table's title belongs in a caption element, directly after the opening table tag. Screen readers announce the caption when the listener arrives at the table and show it in their list of tables. A title typed into a merged cell on the first row looks the same but is read as a data cell, and it pushes the real header row down so the first row of the table is not the headers any more.

Websight does not run this check. axe-core marks the table-fake-caption rule as experimental, and rules with that tag are skipped when scanning by WCAG level. Other tools may still report it as Data or header cells must not be used to give caption to a data table.. Test it by hand using the steps below.

Who it affects

Someone using a screen reader to book the village hall jumps between the three tables on the page by pressing T and hears table, 5 rows, 3 columns, three times. No names, so no way to tell the bookings table from the hire charges.

Someone with a memory impairment reading a long page scrolls back up and cannot see at a glance what each table is, because the title is styled like a cell and blends in.

A sighted keyboard user with a screen reader for support hears the title read as the first cell of the first row, then hears the Day heading announced as part of row two.

Broken versus fixed

Both examples are live and rendered in a sandboxed frame. The broken one is deliberately broken.

Broken

Arriving at the table a screen reader says: table with 5 rows and 3 columns. The title is just another cell, and the Day header is on row two.

Fixed

Announced: table, Hall bookings, week of 3 November, 4 rows and 3 columns. The listener knows what the table is before reading a cell.

Show the markup behind both examples
Broken
<style>body{font-family:system-ui;margin:0;padding:12px}table{border-collapse:collapse;font-size:14px}td,th{border:1px solid #bbb;padding:6px 10px;text-align:left}th{background:#fee}</style>
<table>
  <tr><td colspan="3"><strong>Hall bookings, week of 3 November</strong></td></tr>
  <tr><th scope="col">Day</th><th scope="col">Morning</th><th scope="col">Evening</th></tr>
  <tr><th scope="row">Monday</th><td>Toddler group</td><td>Yoga</td></tr>
  <tr><th scope="row">Tuesday</th><td>Free</td><td>Parish council</td></tr>
  <tr><th scope="row">Wednesday</th><td>Art club</td><td>Badminton</td></tr>
</table>
Fixed
<style>body{font-family:system-ui;margin:0;padding:12px}table{border-collapse:collapse;font-size:14px}td,th{border:1px solid #bbb;padding:6px 10px;text-align:left}th{background:#fee}caption{text-align:left;font-weight:bold;padding:6px 0}</style>
<table>
  <caption>Hall bookings, week of 3 November</caption>
  <tr><th scope="col">Day</th><th scope="col">Morning</th><th scope="col">Evening</th></tr>
  <tr><th scope="row">Monday</th><td>Toddler group</td><td>Yoga</td></tr>
  <tr><th scope="row">Tuesday</th><td>Free</td><td>Parish council</td></tr>
  <tr><th scope="row">Wednesday</th><td>Art club</td><td>Badminton</td></tr>
</table>

Test it by hand in two minutes

  1. Look at the first row. If one cell stretches across the whole table and holds a title, it is a fake caption.
  2. Right click the title and choose Inspect. It should be a caption element, not a td or th with colspan.
  3. With VoiceOver, open the rotor (Ctrl+Option+U) and choose Tables: each table should be listed by its caption. With NVDA, press T to jump between tables and listen for the caption.
  4. Check the first real row uses th cells. A fake caption often hides that the headers underneath are plain td.

The fix

Before
<table>
  <tr><td colspan="3"><strong>Hall bookings, week of 3 November</strong></td></tr>
  <tr><th scope="col">Day</th><th scope="col">Morning</th><th scope="col">Evening</th></tr>
After
<table>
  <caption>Hall bookings, week of 3 November</caption>
  <tr><th scope="col">Day</th><th scope="col">Morning</th><th scope="col">Evening</th></tr>

The caption goes straight after the opening table tag. Style it however you like; it does not have to look like a caption.

Heading already sits above the table

Before
<h3>Hall bookings</h3>
<table>
  <tr><td colspan="3">Hall bookings</td></tr>
After
<h3 id="bookings">Hall bookings</h3>
<table aria-labelledby="bookings">

If you do not want to repeat the heading, point the table at it with aria-labelledby. A caption is still the simplest option.

Caption that must not be visible

Before
<caption style="display:none">Hire charges</caption>
After
<caption class="visually-hidden">Hire charges</caption>

display:none hides it from screen readers too. A visually-hidden class (clip it to 1px off screen) keeps it for them.

Common mistakes

  • Putting the title in a th with colspan, which is just as fake.
  • Using the summary attribute, which was dropped in HTML5 and most screen readers ignore.
  • Hiding the caption with display:none, which removes it for everyone.
  • Writing the title as a paragraph straight after the table instead of a caption.

WCAG criteria this rule maps to

axe-core tags this rule as WCAG 2.2 level A. It is a check (not run by Websight) for:

The scanner looks for a table with no caption element whose first or last row is a single cell spanning the table and holding text. It cannot tell whether that cell was meant as a title or is a genuine data cell. This check is tagged experimental in axe-core. Automated tools find only part of the accessibility problems on a page; a pass on this rule is not a pass on the criterion.

Scan your own page for the 64 checks Websight does run

Open any page in Chrome, click Websight, and get a score out of 100 with every issue explained and a draft fix snippet per element.

Add Websight to Chrome

Free scans, no account, nothing leaves your computer. Automated tools find only part of the problems; the extension says so on every report.