Spanning Cells (colspan, rowspan)
Sometimes a table cell needs to logically cover more than one column or row — a merged header
spanning several sub-columns, or a category label spanning several rows underneath it. The{' '}
colspan and rowspan attributes handle exactly this, but they also introduce real
accessibility complexity that plain tables don't have.
colspan — Merging Columns
colspan makes a single cell stretch across multiple columns. The value is the number of
columns it should occupy.
<table>
<tr>
<th colspan="3">Q1 2024 Sales Summary</th>
</tr>
<tr>
<th scope="col">Region</th>
<th scope="col">Units</th>
<th scope="col">Revenue</th>
</tr>
<tr>
<td>West</td>
<td>1,204</td>
<td>$180,600</td>
</tr>
</table>rowspan — Merging Rows
rowspan makes a single cell stretch down across multiple rows — commonly used for a category
label that groups several rows beneath it.
<table>
<tr>
<th scope="col">Category</th>
<th scope="col">Item</th>
<th scope="col">Price</th>
</tr>
<tr>
<th scope="rowgroup" rowspan="2">Beverages</th>
<td>Coffee</td>
<td>$3.50</td>
</tr>
<tr>
<td>Tea</td>
<td>$3.00</td>
</tr>
</table>rowspan, the following row(s) have one fewer cell at that position — the spanned cell already occupies that slot. This is the single most common source of "my table columns are misaligned" bugs.Complex Layouts: Merged Header Grids
Combining colspan and rowspan in the header lets you build grouped, two-level column
headers — for example, "Q1" spanning three sub-columns (Jan, Feb, Mar), each of which spans
two rows only where there's no further breakdown.
<table>
<thead>
<tr>
<th rowspan="2" scope="col">Product</th>
<th colspan="3" scope="colgroup">Q1</th>
<th colspan="3" scope="colgroup">Q2</th>
</tr>
<tr>
<th scope="col">Jan</th>
<th scope="col">Feb</th>
<th scope="col">Mar</th>
<th scope="col">Apr</th>
<th scope="col">May</th>
<th scope="col">Jun</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Widget A</th>
<td>120</td><td>134</td><td>150</td>
<td>145</td><td>160</td><td>172</td>
</tr>
</tbody>
</table>headers/id attribute pair (covered on the next page) exists to solve.Accessibility Challenges With Spanned Cells
scope works well for simple tables, but it can only point a cell at "the whole row" or "the
whole column" — it can't express "this data cell relates to the Jan column AND the Q1 group
header AND the Widget A row" all at once. For that level of precision, complex tables use the{' '}
headers/id attribute association instead of (or alongside) scope.
Table complexity | Recommended approach |
|---|---|
Simple: one row of column headers, no spans | scope="col" / scope="row" is enough |
Moderate: a few spanning cells, single-level headers | scope="colgroup" / scope="rowgroup" on the spanning header cells |
Complex: multi-level headers, spans in both directions | headers/id explicit association on every data cell |
scope is sufficient. If you find yourself needing to say "and also the group header above that", it's time for headers/id.Preview: headers/id for the Widget A / Jan Cell
<table>
<tr>
<th id="product">Product</th>
<th id="q1">Q1</th>
<th id="jan" headers="q1">Jan</th>
</tr>
<tr>
<th id="widget-a" headers="product">Widget A</th>
<td headers="widget-a jan q1">120</td>
</tr>
</table>The headers attribute on the data cell lists the ids of every header that describes it —
row header, sub-column header, and group header together. This is covered in full on the
Accessible & Responsive Tables page.
Key Takeaways
colspan merges a cell across multiple columns; rowspan merges it across multiple rows.
A spanned cell reduces the number of cells needed in the rows/columns it covers — miscounting this is the most common table bug.
scope="colgroup"/"rowgroup" extends scope to work with a spanning header cell.
Complex, multi-level header tables often need explicit headers/id association rather than relying on scope alone.
If a data cell relates to more than one header in each direction, reach for headers/id.