⚡IntermediateAdvanced Tables

Complex Table Accessibility

Make complex tables with multi-level headers accessible using scope, id, and headers attributes.

Overview

Simple tables with one header row and one header column are handled well by the scope attribute on <th>. Complex tables with multi-level headers, spanning headers, or irregular structures require explicit cell-to-header associations using the headers attribute. The technique: 1. Give every <th> element a unique id 2. On every data <td>, list all the header ids that apply to it in a headers attribute This creates explicit relationships that screen readers can announce: "Q1 Revenue Cell value: $42,000" when the screen reader moves to that cell.

Key Points

  • •scope="col" links a th to its column; scope="row" links it to its row
  • •scope="colgroup" / scope="rowgroup" extend scope to a group of columns/rows
  • •For complex tables: give every <th> a unique id, then reference them in td headers=""
  • •The headers attribute takes a space-separated list of th id values
  • •Don't use complex table markup where a simpler structure would work
  • •The <caption> element provides the accessible name for the entire table

Examples

Two-tier column headers with scope="colgroup"

<table>
  <caption>Quarterly Revenue by Region</caption>
  <thead>
    <tr>
      <th rowspan="2" scope="col" id="region">Region</th>
      <th colspan="2" scope="colgroup" id="h1">H1</th>
      <th colspan="2" scope="colgroup" id="h2">H2</th>
    </tr>
    <tr>
      <th scope="col" id="q1">Q1</th>
      <th scope="col" id="q2">Q2</th>
      <th scope="col" id="q3">Q3</th>
      <th scope="col" id="q4">Q4</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row" id="north">North</th>
      <td headers="north h1 q1">$42K</td>
      <td headers="north h1 q2">$51K</td>
      <td headers="north h2 q3">$48K</td>
      <td headers="north h2 q4">$59K</td>
    </tr>
    <tr>
      <th scope="row" id="south">South</th>
      <td headers="south h1 q1">$38K</td>
      <td headers="south h1 q2">$43K</td>
      <td headers="south h2 q3">$41K</td>
      <td headers="south h2 q4">$52K</td>
    </tr>
  </tbody>
</table>

headers="north h1 q1" → screen reader: "North, H1, Q1: $42K"

Simple scope usage (most tables only need this)

<table>
  <thead>
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Department</th>
      <th scope="col">Salary</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Alice</th>
      <td>Engineering</td>
      <td>$95,000</td>
    </tr>
    <tr>
      <th scope="row">Bob</th>
      <td>Marketing</td>
      <td>$75,000</td>
    </tr>
  </tbody>
</table>

Most tables only need scope. The headers attribute is for complex multi-level headers.

Common Mistakes

  • Using the headers attribute on all tables regardless of complexity scope is simpler and sufficient for most tables
  • Assigning non-unique ids to th elements the headers association breaks
  • Building complex tables where a simpler layout would communicate the same data

Note

Use the WebAIM screen reader testing tool (or NVDA/VoiceOver) to verify complex tables. Navigate with arrow keys the screen reader should announce meaningful context for every cell.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What does scope="rowgroup" on a <th> mean?