Accessible Tables: scope, headers and id

A table a screen reader can read is a table with named headers. scope handles simple tables, headers and id handle the hard ones.

Concept

Someone looking at a table reads a cell and glances up and left to see what it means. A screen reader user cannot glance. The browser has to be told which header applies to which cell, and it announces those headers every time the reader enters a new cell.

Marking a cell as th is the first half. Declaring what that header covers is the second, and there are two mechanisms:

  • scope - for simple tables. Says which direction a header applies in.
  • headers and id - for complex tables. Names exactly which headers apply to each cell.

scope

ValueMeaning
colLabels every cell below it in this column
rowLabels every cell to its right in this row
colgroupLabels the columns it spans
rowgroupLabels the rows it spans
<table>
  <caption>Fee structure by course and year</caption>
  <thead>
    <tr>
      <td></td>
      <th scope="col">Year 1</th>
      <th scope="col">Year 2</th>
      <th scope="col">Year 3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Design</th>
      <td>48,000</td><td>52,000</td><td>56,000</td>
    </tr>
    <tr>
      <th scope="row">Data science</th>
      <td>54,000</td><td>58,000</td><td>62,000</td>
    </tr>
  </tbody>
</table>

Moving to the cell holding 58,000, a screen reader announces Data science, Year 2, 58,000. Without scope it may still guess correctly in a simple table, but the guess is the browser heuristic and it fails as soon as the table gets interesting. Declare it.

headers and id

When a cell is governed by headers that scope cannot describe - two header rows, headers in the middle of the table, irregular spans - each header gets an id and each data cell lists the ids that apply to it.

<table>
  <caption>Marks by subject and assessment</caption>
  <thead>
    <tr>
      <td></td>
      <th id="sem1" colspan="2">Semester 1</th>
      <th id="sem2" colspan="2">Semester 2</th>
    </tr>
    <tr>
      <td></td>
      <th id="s1th" headers="sem1">Theory</th>
      <th id="s1pr" headers="sem1">Practical</th>
      <th id="s2th" headers="sem2">Theory</th>
      <th id="s2pr" headers="sem2">Practical</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th id="design">Design</th>
      <td headers="design sem1 s1th">72</td>
      <td headers="design sem1 s1pr">81</td>
      <td headers="design sem2 s2th">75</td>
      <td headers="design sem2 s2pr">88</td>
    </tr>
  </tbody>
</table>

The cell holding 81 is now announced as Design, Semester 1, Practical, 81. The headers attribute takes a space separated list of ids, and the order you write them is the order they are read.

It is verbose and it must be maintained by hand, which is exactly why it is a last resort rather than a default.

Which to use

TableUse
One header rowscope="col"
Header row and header columnscope="col" and scope="row"
Spanning headers, one levelAdd scope="colgroup" and scope="rowgroup"
Two header levels or irregular structureheaders and id
Genuinely complicatedSplit it into two tables

Other things a table needs

A caption

The accessible name of the table. Always present.

A summary for complex tables

The old summary attribute is obsolete. Describe the structure in a paragraph before the table, or inside the caption:

<caption>
  Marks by subject and assessment
  <span class="caption-note">Four columns, grouped into two semesters,
  each showing theory and practical marks.</span>
</caption>

Do not leave header cells empty

The corner cell of a cross tabulated table is the exception and should be a td. Any other empty th leaves a column with no name.

Keep a scrolling table keyboard reachable

A table in a horizontally scrolling container must be scrollable by keyboard, which means the container needs a tab stop and a name:

<div class="table-scroll" tabindex="0" role="region" aria-label="Fee structure">
  <table>...</table>
</div>

Important rules

  • Every id must be unique in the document.
  • A cell may reference several headers; separate the ids with spaces.
  • Do not mix scope and headers on the same table. Pick one.
  • The summary attribute is obsolete. Do not use it.
  • role="presentation" on a table removes its table semantics entirely, which is only correct for a legacy layout table you cannot rewrite.

Common mistakes

  • Relying on the browser to guess headers rather than declaring scope.
  • Using headers with an id that does not exist, or that is misspelt.
  • Applying headers to some cells and not others.
  • Leaving a header cell empty.
  • Omitting the caption.
  • Building a table so complex that no markup can rescue it.

Best practices

  • Caption and scope on every table. That covers the overwhelming majority of real tables.
  • Reach for headers and id only when the structure genuinely demands it.
  • Prefer splitting a complex table over annotating it.
  • Test with a screen reader by moving cell to cell and listening to what is announced.
  • Wrap wide tables in a labelled, focusable scroll container.

Practice

  1. Add scope to a table with headers on both axes, then listen to a middle cell with a screen reader.
  2. Build a two level header table using headers and id, and confirm each data cell announces all three headers.
  3. Take the same table and split it into two simple ones. Compare how they read.
  4. Wrap a wide table in a scroll container that a keyboard can reach and scroll.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Software Engineer Notes Management System Administrator

Continue reading

All HTML notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.