HTMLSpanning Cells (colspan, rowspan)

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.

HTML
<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.

HTML
<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>
Note
Once a cell uses 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.

HTML
<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>
Warning
Merged header grids like this are common in reports and dashboards, but they're genuinely hard for screen reader users to parse from cell position alone. This is exactly the case the 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

Tip
A good rule of thumb: if you can describe a cell's header in one sentence using only "its row" and "its column", 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

HTML
<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
  1. colspan merges a cell across multiple columns; rowspan merges it across multiple rows.

  2. A spanned cell reduces the number of cells needed in the rows/columns it covers — miscounting this is the most common table bug.

  3. scope="colgroup"/"rowgroup" extends scope to work with a spanning header cell.

  4. Complex, multi-level header tables often need explicit headers/id association rather than relying on scope alone.

  5. If a data cell relates to more than one header in each direction, reach for headers/id.