HTML Table Headers and Captions

Published Updated

Headers and a caption turn a raw grid of cells into a table that names its own columns, groups its rows, and reads correctly to a screen reader.

Syntax

<table>
  <caption>Table title</caption>
  <thead>
    <tr>
      <th scope="col">Heading</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Data</td>
    </tr>
  </tbody>
</table>

Header Cells with th

The th element marks a cell as a header instead of plain data. Browsers render it bold and centered by default, but the real work it does is semantic, it tells assistive technology that this cell labels the data cells near it. Swap a td for a th anywhere a cell is acting as a label rather than a value.

<table>
  <tr>
    <th>Drink</th>
    <th>Price</th>
  </tr>
  <tr>
    <td>Espresso</td>
    <td>3.00</td>
  </tr>
</table>

Headers do not have to sit only in the top row. A table can also have a header at the start of each row, for example a row that begins with a day name and then lists values for that day.

Gotcha: Looking Like a Header Is Not Being One

A td styled with bold, centered text can look exactly like a th on screen, and that similarity is the trap. Assistive technology does not read your CSS, it reads the element, so a bold td still announces as plain data with no column or row it labels. Keep th for anything that is genuinely a header, and restyle it with CSS instead of swapping it for a td that only looks the part.

Grouping Rows with thead, tbody, and tfoot

Once a table has more than a handful of rows, you can group them into three sections. thead holds the header row, tbody holds the main data rows, and tfoot holds a summary row like a total. The grouping makes your markup readable, and it lets the browser keep the header in view when a long table scrolls or prints across pages.

<table>
  <thead>
    <tr>
      <th scope="col">Drink</th>
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Espresso</td>
      <td>3.00</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <td>Total</td>
      <td>3.00</td>
    </tr>
  </tfoot>
</table>

You can write tfoot before or after tbody in your markup and the browser still renders it at the bottom, so put it wherever reads clearest to you.

A short table of three or four rows does not need the full split. A bare header row followed by data rows is still valid, complete markup on its own. Reach for thead, tbody, and tfoot once a table grows long enough to scroll or picks up a summary row, because that length is when the extra sectioning actually earns its keep.

The Scope Attribute

A header labels either a column or a row, and scope says which. Set scope="col" on a header that titles the column below it, and scope="row" on a header that titles the row beside it. Sighted readers infer the direction from position, but a screen reader needs scope spelled out to read the right header before each value.

<table>
  <thead>
    <tr>
      <th scope="col">Day</th>
      <th scope="col">Hours</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Monday</th>
      <td>Closed</td>
    </tr>
  </tbody>
</table>

Here the top cells scope to their columns and the day cell scopes to its row, so a screen reader can announce both the column and the row header for any value. Add scope to every header cell and the table stays readable when nobody can see the grid.

Some screen readers make a reasonable guess on a small, simple table even without scope, but that guess breaks down once a table has more than one header row or mixes row headers with column headers. Writing scope explicitly costs nothing and removes the guesswork entirely.

The Caption Names the Table

The caption element gives the table a visible title, and it must be the very first child inside table, before any rows. It reads out as the table's name to assistive technology, so a reader who jumps between tables knows which one they landed on.

<table>
  <caption>Opening hours this week</caption>
  <thead>
    <tr>
      <th scope="col">Day</th>
      <th scope="col">Hours</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Monday</th>
      <td>Closed</td>
    </tr>
  </tbody>
</table>

Example

<table>
  <caption>Weekend coffee prices</caption>
  <thead>
    <tr>
      <th scope="col">Drink</th>
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Espresso</th>
      <td>3.00</td>
    </tr>
    <tr>
      <th scope="row">Cold brew</th>
      <td>4.50</td>
    </tr>
  </tbody>
</table>

This renders with the caption above the grid, a bold header row, and a bold header cell starting each data row. A screen reader announces the caption, then reads each price with both its column header and its row header, so "Espresso, Price, 3.00" comes through in order.

Frequently Asked Questions

Is a caption the same as a heading above the table?

No. A caption is part of the table and is announced with it, so assistive technology knows the name belongs to that data. A heading above the table sits outside it, and nothing connects the two.

What is the headers attribute and when do you need it?

It names the id of each header cell that applies to a data cell, which handles tables too complex for scope alone, such as several header rows or irregular spans. If a table needs it, consider whether two simpler tables would serve the reader better.

Is the summary attribute still valid on a table?

No. summary was removed in HTML5 and assistive technology no longer relies on it. Put the explanation somewhere everyone can read it: a caption for the short name, and a sentence before the table for anything longer.

Can you hide a caption visually but keep it for screen readers?

Yes, with a visually-hidden utility class that keeps the element in the accessibility tree. Do not use display:none or visibility:hidden, because both remove it from that tree and the table loses its name entirely.

Should a table used purely for layout have headers?

A layout table should not exist. If one is unavoidable in an email template, give it role="presentation" and no headers or caption, so assistive technology does not announce a data relationship that is not there.

Sources

  1. [1]
    The Table Header element
    (developer.mozilla.org)
  2. [2]
    The Table Caption element
    (developer.mozilla.org)
  3. [3]
  4. [4]