Complex Tables Done Right
Merge cells with colspan and rowspan, and make headers meaningful with scope.
Data tables get hard when a single value applies to several columns or rows. Two attributes cover almost every real-world grid.
Spanning cells
colspan="n"— the cell stretches across n columns.rowspan="n"— the cell stretches down n rows.
When a cell spans, delete the cells it now covers — a classic bug is leaving them in and pushing the grid out of alignment.
The attribute nobody sets: scope
On a header cell, scope="col" or scope="row" tells assistive tech which cells that header describes. In a plain grid the browser can guess, but the moment you have both row and column headers, scope is what keeps a screen reader from reading nonsense.
<th scope="col">Q1</th>
<th scope="row">Revenue</th>Add a <caption> too — it's the table's accessible name and costs one line.
Example
When to use it
- A complex schedule grid uses scope="col" and scope="row" on <th> elements so screen readers announce the correct header for each cell.
- A data table uses headers and id attributes to explicitly link cells to their column and row headers in complex merged layouts.
- An accessibility audit flags tables missing scope attributes as failures; a developer adds them to achieve WCAG 1.3.1 compliance.
More examples
Table headers with scope attributes
scope="col" on thead headers and scope="row" on row headers explicitly map each data cell to both its headers.
<table>
<caption>Course Enrollment by Term</caption>
<thead>
<tr>
<th scope="col">Course</th>
<th scope="col">Spring</th>
<th scope="col">Fall</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">HTML</th>
<td>142</td>
<td>208</td>
</tr>
<tr>
<th scope="row">CSS</th>
<td>110</td>
<td>189</td>
</tr>
</tbody>
</table>Table with id and headers for complex layout
The headers attribute references multiple th ids, allowing screen readers to read all applicable headers for a cell.
<table>
<tr>
<th id="loc">Location</th>
<th id="q1">Q1</th>
<th id="q2">Q2</th>
</tr>
<tr>
<th id="north">North</th>
<td headers="north q1">$12k</td>
<td headers="north q2">$15k</td>
</tr>
</table>Summary table with colgroup styling
<colgroup> and <col> set column widths; scope attributes on every header ensure screen reader accessibility.
<table>
<colgroup>
<col style="width:40%">
<col style="width:30%">
<col style="width:30%; font-weight:bold;">
</colgroup>
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col">Unit Price</th>
<th scope="col">Total</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Pro Plan</th>
<td>$19/mo</td>
<td>$228/yr</td>
</tr>
</tbody>
</table>
Discussion