Himanshu SharmaSoftware engineer
Let’s talk

HTML · Lesson 21 of 31

Table headers and captions

Make tables accessible with caption, th and scope, headers for complex tables, and colgroup, so screen readers can announce each cell with its labels.

11 minTablesUpdated 18 October 2026

By the end of this lessonAdd a caption and correctly scoped header cells so assistive technology reads each cell of a table together with its row and column labels.

Why headers matter

Sighted readers glance at a table and see which column a number belongs to. A screen reader user hears the table one cell at a time. When the cells are marked up well, the reader announces each value with its labels: "Price, Sourdough loaf, 120 rupees". When they are not, the user hears only "120" and has to count columns in their head.

The markup that makes the first experience possible is small: a caption, proper th cells and scope.

caption: a title for the table

html
<table>
  <caption>Bakery prices, March 2026</caption>
  <thead>
    <tr><th scope="col">Item</th><th scope="col">Price</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">Sourdough loaf</th><td>₹120</td></tr>
    <tr><th scope="row">Multigrain bun</th><td>₹25</td></tr>
  </tbody>
</table>

caption must be the first child of table. It appears above the table by default, and assistive technology announces it as the table's name. It is better than a heading above the table because it is tied to the table itself.

th and scope

Use th for every cell that labels other cells, not only in the top row. Then add scope to say which direction the label points:

  • scope="col": this header labels the cells below it.
  • scope="row": this header labels the cells to its right.

In the example, "Sourdough loaf" is a th with scope="row": it is the label for that row, so a screen reader can announce "Sourdough loaf, Price, 120".

Try this table with a screen reader on, or just read the markup and notice how every number has two labels:

html
<table border="1" cellpadding="6">
  <caption>Quarterly sales (₹ thousands)</caption>
  <thead>
    <tr><th></th><th scope="col">Q1</th><th scope="col">Q2</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">North</th><td>120</td><td>135</td></tr>
    <tr><th scope="row">South</th><td>98</td><td>110</td></tr>
  </tbody>
</table>

The empty th in the corner is fine; some authors put a visually hidden label there, such as "Region".

Tables with two levels of headers

Merged headers, such as a "Timings" group over "Opens" and "Closes", are harder: a single scope cannot say that a cell belongs to two header levels. For these, HTML has a more explicit tool. Give each header an id, and list the ids that apply to a data cell in its headers attribute:

html
<table>
  <caption>Opening hours</caption>
  <tr>
    <th id="day" rowspan="2">Day</th>
    <th id="timings" colspan="2">Timings</th>
  </tr>
  <tr>
    <th id="open">Opens</th>
    <th id="close">Closes</th>
  </tr>
  <tr>
    <td headers="day">Monday</td>
    <td headers="timings open">9:00</td>
    <td headers="timings close">18:00</td>
  </tr>
</table>

If you can, avoid tables this complicated: two simpler tables are easier for everyone, and for you to maintain.

colgroup and col

When you want to style or mark whole columns, colgroup and col describe them without repeating attributes on every cell:

html
<table>
  <colgroup>
    <col>
    <col span="2" style="background:#f3f4f6">
  </colgroup>
  …
</table>

They are optional, and only a handful of CSS properties (such as background and width) work on columns.

Summary text and size

Not every table needs more than a caption. If the table is complex, a short paragraph before it explaining how to read it helps everyone: "Rows show regions, columns show quarters." That is better than the old summary attribute, which is obsolete.

Common mistakes

  • Using td with bold styling instead of th. It looks right and is wrong for assistive technology.
  • No caption on a table that stands alone on a page.
  • Headers with no scope in tables that have row headers.
  • Empty header cells with no label at all in the first column. Even a hidden label is better.
  • Splitting one table into several to save work, instead of one table with consistent headers.

Exercise

Make a table of three students with their marks in Maths and Science. Include a caption, column headers with scope="col", and each student's name as a row header with scope="row".

Show one possible answer
html
<table>
  <caption>Test results, Class 8</caption>
  <thead>
    <tr><th scope="col">Student</th><th scope="col">Maths</th><th scope="col">Science</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">Aarav</th><td>88</td><td>92</td></tr>
    <tr><th scope="row">Meera</th><td>95</td><td>90</td></tr>
    <tr><th scope="row">Kabir</th><td>76</td><td>81</td></tr>
  </tbody>
</table>

Key points

  • caption gives a table a visible title that is also programmatically linked to it.
  • Use th for every cell that labels others, and add scope="col" or scope="row" so the direction is clear.
  • A screen reader announces a data cell together with its row and column headers when th is used correctly.
  • For tables with two levels of headers, the headers and id attributes link each cell to all of its labels.
  • A table with no headers is a table that a screen reader user has to count their way through.

Check yourself

1. What does caption do?
2. What does scope="row" on a th mean?
3. A table uses td for its top row of labels. What is the problem?
4. Where must caption go inside a table?

Free tools for this lesson