HTML colspan and rowspan
The colspan and rowspan attributes let a single table cell stretch across multiple columns or multiple rows, so a table’s grid doesn’t have to be perfectly rectangular. They’re essential whenever a heading needs to sit above several columns, or a single value needs to apply to several rows at once — similar to “merged cells” in a spreadsheet. Used well, they make complex tables like schedules, pricing grids, and multi-level headers far easier to read; used carelessly, they silently break a table’s column alignment and confuse assistive technology.
Overview / How it works
An HTML table is built from rows (tr) that each contain cells (td for data, th for headers). By default, every cell occupies exactly one column and one row, and the browser lines cells up into a strict grid: the first cell of every row belongs to column 1, the second to column 2, and so on. The colspan and rowspan attributes change how much grid space a single cell consumes.
colspan tells the browser “this cell should occupy N columns instead of one,” so the cells that follow it in the same row shift over and effectively skip those columns. rowspan tells the browser “this cell should occupy N rows instead of one,” so the row(s) beneath it must have one fewer cell at that position — the spanned cell already fills that slot, and if you don’t remove the corresponding cell from the row below, the whole table shifts out of alignment.
Both attributes take a positive integer. A value of 1 is the same as omitting the attribute. Browsers also accept 0 for colspan in table-layout algorithms to mean “span to the last column of the column group,” but this is a rarely-used, poorly-supported edge case — in practice you should always give colspan and rowspan an explicit, correct number.
Under the hood, the browser’s HTML parser builds the DOM tree exactly as written — it doesn’t validate that your colspan/rowspan numbers add up correctly. Only later, during the table layout algorithm (the same algorithm described in the HTML spec’s “forming a table” section), does the browser walk each row, account for cells still “owed” from spans started in earlier rows, and slot each remaining cell into the next free column. If your span counts are wrong, the layout algorithm still runs — it just produces a lopsided, visually broken grid rather than throwing an error you’d notice in the markup.
Both attributes are purely structural, not visual — they describe how many grid cells a cell occupies, not how it looks. Borders, background colors, and cell padding are styling concerns that belong in CSS, not in the table markup itself.
Syntax
<table>
<tr>
<th colspan="2">Header spanning two columns</th>
</tr>
<tr>
<td rowspan="2">Spans two rows</td>
<td>Row 1, remaining column</td>
</tr>
<tr>
<td>Row 2, remaining column</td>
</tr>
</table>
| Attribute | Applies to | Value | Effect |
|---|---|---|---|
colspan |
td, th |
Positive integer (e.g. 2, 3) |
Cell occupies that many columns; later cells in the same row shift right |
rowspan |
td, th |
Positive integer (e.g. 2, 3) |
Cell occupies that many rows; the corresponding cell must be omitted from the following row(s) |
Neither attribute is required for a table to be valid — omit them and every cell defaults to spanning exactly one column and one row.
Examples
Example 1: A colspan header
<table>
<tr>
<th colspan="3">Quarterly Sales Report</th>
</tr>
<tr>
<th>Region</th>
<th>Units Sold</th>
<th>Revenue</th>
</tr>
<tr>
<td>North</td>
<td>120</td>
<td>$4,800</td>
</tr>
</table>
Result: The browser renders a three-row table. The first row shows a single wide header cell, “Quarterly Sales Report”, stretching across all three columns. The second row shows three normal header cells (“Region”, “Units Sold”, “Revenue”), and the third row shows the matching data. All three columns line up correctly because the first row has exactly one cell with colspan="3", matching the three columns used in every other row.
This is the most common use of colspan: a title or grouping label that sits above several regular columns. Notice that the row with the spanning cell still only needs one th element — the browser handles the widening automatically based on the attribute value.
Example 2: A rowspan cell
<table>
<tr>
<th>Day</th>
<th>Time</th>
<th>Class</th>
</tr>
<tr>
<td rowspan="2">Monday</td>
<td>9:00 AM</td>
<td>Algebra</td>
</tr>
<tr>
<td>11:00 AM</td>
<td>Chemistry</td>
</tr>
</table>
Result: A three-column, three-row table renders where the word “Monday” appears once, in a tall cell that visually spans both the 9:00 AM and 11:00 AM rows. Each of those two rows only supplies two more cells (the time and the class), since the “Day” column position for the second row is already filled by the spanning cell from the row above.
This is the key rule to internalize: when a cell uses rowspan="2", the very next row must have one fewer td than the header row, because that column slot is already occupied. Forgetting to remove that cell is the single most common rowspan mistake, covered below.
Example 3: Combining colspan and rowspan
<table>
<tr>
<th>Product</th>
<th colspan="2">Price by Size</th>
</tr>
<tr>
<th></th>
<th>Small</th>
<th>Large</th>
</tr>
<tr>
<td rowspan="2">Coffee</td>
<td>$2.50</td>
<td>$3.50</td>
</tr>
<tr>
<td>$2.75</td>
<td>$3.75</td>
</tr>
</table>
Result: A four-row, three-column table renders with a two-level header: “Product” sits alone in the first column (spanning both header rows implicitly through the empty second-row cell), while “Price by Size” spans two columns above “Small” and “Large”. Below that, “Coffee” appears once in a tall cell spanning two price rows, next to four individual price cells.
This realistic pricing table shows colspan and rowspan working together: colspan groups related columns under one label, while rowspan groups related rows under one label, and the total column count (three) stays consistent across every row once spans are accounted for.
How it works step by step / Under the hood
When the browser lays out a table, it processes rows top to bottom and, within each row, cells left to right, using an internal grid of column slots:
- Step 1: The parser reads each
trand its childtd/thelements into the DOM tree exactly as written, without checking whether spans line up. - Step 2: During layout, the browser tracks which grid slots are already “claimed” by a
rowspanfrom a previous row. - Step 3: For the current row, it walks the actual
td/thelements in source order and drops each one into the next unclaimed column slot, skipping any slots still claimed by an active rowspan from above. - Step 4: If a cell has
colspan="N", it claims N consecutive slots in the current row. - Step 5: If a cell has
rowspan="N", its column slot(s) stay claimed for the next N−1 rows, meaning those rows must supply one fewer cell at that position. - Step 6: Once every row is processed, the browser knows the table’s total column count (the widest row) and can compute column widths and render the final grid.
Because this all happens after parsing, a browser will never reject malformed span markup outright — it will render something, just not the grid you intended, which is why testing your tables visually (not just checking that the markup parses) matters.
Common Mistakes
Mistake 1: Forgetting to omit the spanned cell in the next row
<table>
<tr>
<td rowspan="2">Monday</td>
<td>9:00 AM</td>
</tr>
<tr>
<td>Monday</td>
<td>11:00 AM</td>
</tr>
</table>
This is wrong because the first cell already claims the “Day” column slot for both rows via rowspan="2". Adding a second “Monday” cell in the next row pushes every cell in that row one column to the right, so “Monday” ends up next to “9:00 AM” incorrectly and the table gains an extra, unintended column. The fix is to simply remove the redundant cell, since the rowspan already covers it:
<table>
<tr>
<td rowspan="2">Monday</td>
<td>9:00 AM</td>
</tr>
<tr>
<td>11:00 AM</td>
</tr>
</table>
Mistake 2: colspan value that doesn’t match the real column count
<table>
<tr>
<th colspan="4">Order Summary</th>
</tr>
<tr>
<td>Item</td>
<td>Qty</td>
<td>Price</td>
</tr>
</table>
Here the header claims colspan="4", but the row below it only ever has three real columns (Item, Qty, Price). The browser will still render a fourth, empty-looking column to satisfy the header’s span, leaving a stray blank column running down the whole table. Always count the actual columns in your widest data row first, then set colspan to match:
<table>
<tr>
<th colspan="3">Order Summary</th>
</tr>
<tr>
<td>Item</td>
<td>Qty</td>
<td>Price</td>
</tr>
</table>
Best Practices
- Before writing spans, sketch (even on paper) how many columns the table actually has, then make sure every row’s cells plus their span values add up to that same total.
- Use
colspan/rowspanonly for genuine merged data — don’t reach for them just to fake visual spacing; that’s a job for CSS. - When a
rowspancell removes a column slot from a following row, add a code comment or, better, keep your markup indentation consistent so the “missing” cell is easy to spot when reading the source later. - Pair complex spanned headers with the
scopeattribute (e.g.scope="colgroup"orscope="rowgroup") onthelements so screen readers can correctly announce which data cells a spanning header describes. - Keep tables used for spans limited to genuinely tabular data — don’t use
colspan/rowspantricks to build page layouts; that’s what CSS layout (grid/flexbox) is for. - Test visually in a real browser after adding spans; a well-formed but miscounted table will still render, just misaligned, and that’s easy to miss by only reading the markup.
Practice Exercises
- Build a 3-column, 3-row table for a weekly workout log where the first row has a single header spanning all three columns reading “Weekly Workout Log”, and the second row has individual headers “Day”, “Exercise”, “Reps”.
- Build a table listing two doctors and their available appointment slots, where each doctor’s name uses
rowspanto span two rows of time slots (so each doctor appears once, next to two different times). - Build a small “Team Roster” table with a
colspan="2"header labeled “Contact Info” sitting above two narrower headers, “Email” and “Phone”, followed by one data row. Double-check that your data row supplies exactly two cells under that spanned header.
Summary
colspanmakes a cell span multiple columns; later cells in that row shift right to make room.rowspanmakes a cell span multiple rows; the corresponding row(s) below must omit that cell entirely.- Both attributes take a positive integer value; omitting them is the same as a value of
1. - The browser never validates that your span counts are internally consistent — mismatched values render as a lopsided table rather than an error.
colspanandrowspanare structural attributes, not styling ones; visual appearance (borders, colors, spacing) belongs in CSS.- Always count actual columns in your widest row and make sure spans plus regular cells add up to that count in every row.
