HTML Guides for table
Learn how to identify and fix common HTML validation errors flagged by the W3C Validator — so your pages are standards-compliant and render correctly across every browser. Also check our Accessibility Guides.
In HTML, the structure of a <table> is implicitly defined by its rows and columns. The first <tr> in a table (or table section like <thead>, <tbody>, or <tfoot>) establishes the expected column count for the entire table. When a subsequent row contains more cells than the first row, the validator raises this error because the table’s column grid becomes inconsistent.
Browsers will still attempt to render mismatched tables, but the results can be unpredictable and problematic. Screen readers and other assistive technologies rely on a well-formed table grid to correctly associate data cells with their headers. An inconsistent column count can cause these tools to misread or skip content, making the table inaccessible to users who depend on them. Additionally, inconsistent tables are harder to style with CSS and can lead to unexpected layout shifts.
There are several common causes for this issue:
- Missing cells in the first row — The first row has fewer <td> or <th> elements than subsequent rows.
- Forgotten colspan — A cell in the first row should span multiple columns but is missing a colspan attribute.
- Extra cells in later rows — A row further down the table has more cells than intended.
- Mismatched colspan arithmetic — The sum of cells and their colspan values doesn’t add up consistently across rows.
To fix this, review every row in the table and ensure the total column count (accounting for colspan and rowspan attributes) is the same for each row.
Examples
Incorrect: second row has more columns than the first
The first row defines 1 column, but the second row has 2 columns.
<table>
<tr>
<td>Liza</td>
</tr>
<tr>
<td>Jimmy</td>
<td>14</td>
</tr>
</table>
Fixed: add missing cells to the first row
Make both rows have 2 columns by adding a header or data cell to the first row.
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Jimmy</td>
<td>14</td>
</tr>
</table>
Fixed: use colspan if the first row intentionally spans the full width
If the first row is meant to be a single spanning header, use colspan to match the total column count.
<table>
<tr>
<th colspan="2">Student Info</th>
</tr>
<tr>
<td>Jimmy</td>
<td>14</td>
</tr>
</table>
Incorrect: later row exceeds column count with extra cells
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Liza</td>
<td>12</td>
<td>Extra cell</td>
</tr>
</table>
Fixed: remove the extra cell or expand the header row
<table>
<tr>
<th>Name</th>
<th>Age</th>
<th>Notes</th>
</tr>
<tr>
<td>Liza</td>
<td>12</td>
<td>Extra cell</td>
</tr>
</table>
Incorrect: colspan mismatch causes inconsistent totals
The first row spans 3 columns total (1 + colspan="2"), but the second row has 4 cells.
<table>
<tr>
<th>Name</th>
<th colspan="2">Contact</th>
</tr>
<tr>
<td>Liza</td>
<td>liza@example.com</td>
<td>555-0100</td>
<td>Room 4</td>
</tr>
</table>
Fixed: adjust colspan or cell count so all rows match
<table>
<tr>
<th>Name</th>
<th colspan="3">Contact & Location</th>
</tr>
<tr>
<td>Liza</td>
<td>liza@example.com</td>
<td>555-0100</td>
<td>Room 4</td>
</tr>
</table>
When debugging this issue, count the effective columns for each row by adding up the number of cells plus any additional columns contributed by colspan values (a colspan="3" cell counts as 3 columns). Every row in the table must produce the same total.
HTML tables establish their column count based on the first row. When subsequent rows have fewer <td> or <th> cells than that initial row defines, the table structure becomes invalid. Browsers will typically render these tables by leaving blank space where the missing cells should be, but the underlying markup is malformed.
This matters for several important reasons. Screen readers and other assistive technologies rely on a consistent table structure to navigate cells and associate data with the correct headers. When cells are missing, users who depend on these tools may receive confusing or incorrect information. Additionally, inconsistent row widths can lead to unpredictable layout behavior across different browsers and make your table markup harder to maintain.
When counting columns, remember that the colspan attribute contributes to the total. A single <td colspan="3"> counts as three columns, not one. So if your first row has two <td> elements and one of them has colspan="2", the table is three columns wide, and every other row must also account for three columns.
How to Fix
There are several approaches depending on your intent:
- Add the missing cells — If data was accidentally omitted, add the appropriate <td> or <th> elements to complete the row.
- Use colspan — If a cell should intentionally span multiple columns, use the colspan attribute so the total column count matches.
- Add empty cells — If a cell simply has no content, include an empty <td></td> to maintain the structure.
Examples
❌ Row with fewer columns than the first row
The first row establishes a 3-column table, but the second row only has 2 cells:
<table>
<tr>
<td>Name</td>
<td>Role</td>
<td>Department</td>
</tr>
<tr>
<td>Alice</td>
<td>Engineer</td>
</tr>
</table>
✅ Fix by adding the missing cell
<table>
<tr>
<td>Name</td>
<td>Role</td>
<td>Department</td>
</tr>
<tr>
<td>Alice</td>
<td>Engineer</td>
<td>Product</td>
</tr>
</table>
✅ Fix by using colspan to span remaining columns
If the second row intentionally has fewer logical cells, use colspan so the total still matches:
<table>
<tr>
<td>Name</td>
<td>Role</td>
<td>Department</td>
</tr>
<tr>
<td>Alice</td>
<td colspan="2">Engineer — General</td>
</tr>
</table>
✅ Fix by adding an empty cell
If there’s simply no data for that column, include an empty cell:
<table>
<tr>
<td>Name</td>
<td>Role</td>
<td>Department</td>
</tr>
<tr>
<td>Alice</td>
<td>Engineer</td>
<td></td>
</tr>
</table>
❌ Mismatch caused by colspan in the first row
Be careful when the first row uses colspan, as it increases the effective column count:
<table>
<tr>
<td colspan="2">Full Name</td>
<td>Role</td>
</tr>
<tr>
<td>Alice</td>
<td>Engineer</td>
</tr>
</table>
Here the first row spans 3 columns (2 + 1), but the second row only has 2 cells.
✅ Fix by matching the full column count
<table>
<tr>
<td colspan="2">Full Name</td>
<td>Role</td>
</tr>
<tr>
<td>Alice</td>
<td>Smith</td>
<td>Engineer</td>
</tr>
</table>
The <table> element in HTML supports a limited set of attributes — primarily global attributes like class, id, and style. The height attribute was never part of the HTML standard for tables (unlike width, which was valid in HTML 4 but has since been deprecated). Despite this, many browsers historically accepted height on <table> as a non-standard extension, which led to its widespread but incorrect use.
Because height is not a recognized attribute on <table>, using it means your markup is invalid and may behave inconsistently across browsers. Some browsers might honor it, others might ignore it entirely, and future browser versions could change their behavior at any time. Relying on non-standard attributes makes your code fragile and harder to maintain.
The fix is straightforward: remove the height attribute from the <table> element and use CSS to set the desired height. You can apply the CSS inline via the style attribute, or better yet, use an external or internal stylesheet with a class or ID selector.
Examples
❌ Invalid: height attribute on <table>
<table height="300">
<tr>
<td>Name</td>
<td>Score</td>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
This triggers the validator error: Attribute “height” not allowed on element “table” at this point.
✅ Fixed: Using inline CSS
<table style="height: 300px;">
<tr>
<td>Name</td>
<td>Score</td>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
✅ Fixed: Using a CSS class (preferred)
<style>
.tall-table {
height: 300px;
}
</style>
<table class="tall-table">
<tr>
<td>Name</td>
<td>Score</td>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
Using a class keeps your HTML clean and makes it easy to adjust the styling later without touching the markup. Note that min-height is often a better choice than height for tables, since table content can naturally grow beyond a fixed height, and min-height ensures the table is at least a certain size without clipping content.
The HTML specification defines a strict content model for tables. A <table> element can only contain <caption>, <colgroup>, <thead>, <tbody>, <tfoot>, and <tr> as direct children. A <tr> element can only contain <td> and <th> elements. When text or inline elements like <span>, <a>, or <strong> appear directly inside any of these table-structural elements, they violate the content model and trigger this validation error.
This matters for several reasons. Browsers handle misplaced table content inconsistently — some will silently move the text before the table, others may discard it, and some may attempt to create anonymous table cells. This leads to unpredictable layouts across browsers and devices. Screen readers rely on correct table structure to navigate cells and announce row/column relationships, so misplaced content can confuse assistive technology users. Additionally, any text placed directly inside a <tr> but outside a cell is technically not part of any row/column structure, making it semantically meaningless in the table context.
Common causes of this error include:
- Accidentally placing text between <tr> and <td> tags
- Placing a heading or paragraph directly inside a <table> (use <caption> instead for table titles)
- Using non-breaking spaces ( ) or other characters directly inside <table> or <tr> for spacing
- Incorrectly nesting non-table elements like <div>, <p>, or <form> as direct children of <table> or <tr>
Examples
Text directly inside a table row
<!-- ❌ Wrong: text is a direct child of <tr> -->
<table>
<tr>
Hello World
<td>First Cell</td>
<td>Second Cell</td>
</tr>
</table>
<!-- ✅ Fixed: text is wrapped in a <td> -->
<table>
<tr>
<td>Hello World</td>
<td>First Cell</td>
<td>Second Cell</td>
</tr>
</table>
Text directly inside a table element
<!-- ❌ Wrong: heading and spacing text placed directly in <table> -->
<table>
Monthly Sales Report
<tr>
<th>Month</th>
<th>Revenue</th>
</tr>
</table>
<!-- ✅ Fixed: use <caption> for table titles -->
<table>
<caption>Monthly Sales Report</caption>
<tr>
<th>Month</th>
<th>Revenue</th>
</tr>
</table>
Non-breaking spaces used for spacing
<!-- ❌ Wrong: directly inside <tr> -->
<table>
<tr>
<td>Data</td>
</tr>
</table>
<!-- ✅ Fixed: use CSS for spacing, or place content in a cell -->
<table>
<tr>
<td>Data</td>
</tr>
</table>
Inline elements directly inside a table
<!-- ❌ Wrong: <a> tag directly inside <table> -->
<table>
<a href="/details">View details</a>
<tr>
<td>Item 1</td>
<td>$10.00</td>
</tr>
</table>
<!-- ✅ Fixed: move the link into a cell or outside the table -->
<table>
<tr>
<td>Item 1</td>
<td>$10.00</td>
<td><a href="/details">View details</a></td>
</tr>
</table>
Content between table sections
<!-- ❌ Wrong: text between <thead> and <tbody> -->
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
No results found
<tbody>
<tr>
<td>—</td>
</tr>
</tbody>
</table>
<!-- ✅ Fixed: place the message inside a cell -->
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>No results found</td>
</tr>
</tbody>
</table>
The HTML specification requires that every row in a table has at least one cell that starts on that row. A “cell beginning on a row” means a <td> or <th> element that is directly placed in that <tr>, as opposed to a cell that merely spans into it via rowspan from an earlier row. When the validator encounters a row with no cells beginning on it, it flags the error because the row is structurally meaningless — it contributes nothing to the table’s data model.
This issue commonly arises in two scenarios:
- Empty <tr> elements — A <tr> that contains no <td> or <th> children at all. This sometimes appears when developers use empty rows for visual spacing, or when content management systems generate leftover markup.
- Rows fully covered by rowspan — When cells in preceding rows use rowspan values large enough to span over an entire subsequent row, that subsequent row ends up with no cells beginning on it, even if it technically “has” cells passing through it.
This matters for several reasons. Screen readers and other assistive technologies rely on a well-formed table structure to navigate cells and announce their content. An empty or fully-spanned row confuses this navigation. Browsers may also handle malformed tables inconsistently, leading to unexpected rendering. Ensuring every row has at least one cell that begins on it keeps your tables semantically correct and accessible.
How to fix it
- Remove empty rows. If a <tr> has no cells and serves no purpose, delete it entirely.
- Add cells to the row. If the row is intentional, populate it with <td> or <th> elements (they can be empty if needed).
- Adjust rowspan values. If previous cells span too many rows, reduce their rowspan so that every row still has at least one cell of its own.
- Use CSS for spacing. If empty rows were used for visual spacing, use CSS margin, padding, or border-spacing instead.
Note that self-closing <tr /> elements are treated the same as <tr></tr> — they produce an empty row and will trigger this error.
Examples
Empty row in <tbody> (incorrect)
<table>
<tbody>
<tr>
</tr>
<tr>
<td>Data</td>
</tr>
</tbody>
</table>
The first <tr> has no cells, so the validator reports that row 1 of the <tbody> row group has no cells beginning on it.
Empty row removed (correct)
<table>
<tbody>
<tr>
<td>Data</td>
</tr>
</tbody>
</table>
Row fully covered by rowspan (incorrect)
<table>
<tbody>
<tr>
<td rowspan="3">Spans three rows</td>
<td rowspan="3">Also spans three</td>
</tr>
<tr>
</tr>
<tr>
</tr>
</tbody>
</table>
Rows 2 and 3 have no cells beginning on them — all cells originate from row 1. Even though cells pass through those rows via rowspan, the validator still requires at least one cell to begin on each row.
Corrected rowspan with cells on every row (correct)
<table>
<tbody>
<tr>
<td rowspan="3">Spans three rows</td>
<td>Row 1 detail</td>
</tr>
<tr>
<td>Row 2 detail</td>
</tr>
<tr>
<td>Row 3 detail</td>
</tr>
</tbody>
</table>
Each row now has at least one <td> that begins on it, satisfying the requirement.
Using CSS instead of empty rows for spacing (correct)
<table>
<tbody>
<tr>
<td>First item</td>
</tr>
<tr style="height: 1.5em;">
<td>Second item with extra space above</td>
</tr>
</tbody>
</table>
Instead of inserting an empty row for spacing, apply CSS to the row or cells that need additional space.
The HTML <table> element follows a rigid structure. A <table> can only contain <caption>, <colgroup>, <thead>, <tbody>, <tfoot>, and <tr> as direct children. A <tr> can only contain <td> and <th> elements. Placing an <a> tag (or any other inline/flow content) directly inside <table>, <tr>, or other table-structural elements violates this content model, triggering the “Start tag a seen in table“ validation error.
This matters for several reasons. Browsers handle invalid table markup inconsistently — some may silently move the misplaced <a> element outside the table entirely, while others may render it in unexpected positions. This leads to unpredictable layouts and broken links. Screen readers and other assistive technologies rely on proper table structure to navigate content, so misplaced elements can make the page confusing or inaccessible. Additionally, search engine crawlers may not correctly associate the link with its intended context.
The fix is straightforward: always wrap inline content like <a> inside a <td> or <th> element. Every piece of visible content in a table must live inside a table cell.
Examples
❌ Incorrect: <a> directly inside <tr>
<table>
<tr>
<a href="/details">View details</a>
</tr>
</table>
The <a> is a direct child of <tr>, which only allows <td> and <th> children.
❌ Incorrect: <a> directly inside <table>
<table>
<a href="/details">View details</a>
<tr>
<td>Data</td>
</tr>
</table>
The <a> is a direct child of <table>, which does not allow inline content.
❌ Incorrect: <a> directly inside <tbody>
<table>
<tbody>
<a href="/details">View details</a>
<tr>
<td>Data</td>
</tr>
</tbody>
</table>
The <a> is placed directly inside <tbody>, which only allows <tr> and <script>/<template> elements.
✅ Correct: <a> inside a <td>
<table>
<tr>
<td>
<a href="/details">View details</a>
</td>
</tr>
</table>
✅ Correct: <a> inside a <th>
<table>
<thead>
<tr>
<th>
<a href="/details">View details</a>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data</td>
</tr>
</tbody>
</table>
✅ Correct: wrapping an entire row’s content in a link
If you want to make an entire row clickable, place the <a> inside each <td> rather than wrapping the <tr>:
<table>
<tr>
<td><a href="/item/1">Product name</a></td>
<td><a href="/item/1">$19.99</a></td>
<td><a href="/item/1">In stock</a></td>
</tr>
</table>
This keeps the table structure valid while still providing clickable content across the row. You can use CSS to remove the visual link styling and make each <a> fill its entire cell for a seamless clickable-row effect.
The <table> element in HTML has a rigid content model. Directly inside <table>, only specific elements are allowed: <caption>, <colgroup>, <thead>, <tbody>, <tfoot>, <tr>, and <script>-supporting elements. Similarly, <tr> elements may only contain <td> and <th> elements. A <br> tag placed between table rows, or directly inside a <tbody> or <tr> but outside a cell, violates this content model.
This error typically happens when developers try to add vertical spacing between table rows using <br> tags. Browsers handle this invalid markup inconsistently — some will push the <br> outside the table entirely, while others may silently ignore it. This leads to unpredictable layout behavior across browsers and can confuse assistive technologies that rely on proper table structure to convey data relationships to users.
The <br> element is only valid inside phrasing content contexts, such as within a <td>, <th>, <p>, <span>, or similar elements. If you need to add spacing between rows, use CSS (margin, padding, or border-spacing) instead of inserting <br> tags into the table structure.
How to fix it
- Remove the <br> if it was added accidentally or as a formatting attempt between rows.
- Move the <br> inside a <td> or <th> if you intended it to create a line break within a cell’s content.
- Use CSS for spacing if you need visual separation between rows. Apply padding to cells or use the border-spacing property on the table.
Examples
❌ Invalid: <br> between table rows
<table>
<tr>
<th>Item</th>
<th>Description</th>
</tr>
<br>
<tr>
<td>Book</td>
<td>A guide to HTML</td>
</tr>
</table>
❌ Invalid: <br> directly inside a <tr>
<table>
<tr>
<br>
<td>Book</td>
<td>A guide to HTML</td>
</tr>
</table>
✅ Fixed: <br> removed, CSS used for spacing
<table style="border-spacing: 0 1em;">
<tr>
<th>Item</th>
<th>Description</th>
</tr>
<tr>
<td>Book</td>
<td>A guide to HTML</td>
</tr>
</table>
✅ Valid: <br> used inside a table cell
A <br> element is perfectly valid inside a <td> or <th>, where it functions as a line break within the cell’s content.
<table>
<tr>
<th>Item</th>
<th>Description</th>
</tr>
<tr>
<td>Book</td>
<td>
Title: HTML & CSS<br>
Author: Jon Duckett
</td>
</tr>
</table>
✅ Fixed: Using padding for row spacing
If your goal is to create visual separation between rows, CSS padding on cells is the cleanest approach:
<style>
.spaced-table td,
.spaced-table th {
padding: 1em 0.5em;
}
</style>
<table class="spaced-table">
<tr>
<th>Item</th>
<th>Description</th>
</tr>
<tr>
<td>Book</td>
<td>A guide to HTML</td>
</tr>
</table>
A td (table cell) start tag must be placed inside a tr (table row) within a table.
The td element represents a cell of a table that contains data. According to HTML specifications, a td must be a child of a tr, which in turn must be a child of a table. Placing a td directly outside a tr or outside a table is invalid and causes the “stray start tag ‘td’” error in the W3C Validator.
Incorrect usage:
<table>
<td>Cell data</td>
</table>
In the above example, the td is not inside a tr.
Correct usage:
<table>
<tr>
<td>Cell data</td>
</tr>
</table>
Here, the td is nested inside a tr, which is correctly within a table. This will resolve the validation error.
When you use the rowspan attribute on a table cell, you’re telling the browser that cell should vertically span across multiple rows. However, each row group element — <thead>, <tbody>, and <tfoot> — acts as a boundary. A cell’s rowspan cannot extend beyond the row group that contains it. If a <tbody> has 2 rows and a cell in the first row declares rowspan="4", the cell tries to span into rows that don’t exist within that group. The validator reports that the cell span is “clipped to the end of the row group.”
This matters for several reasons. First, assistive technologies like screen readers rely on accurate table structure to navigate cells and announce row/column relationships. A rowspan that overshoots its row group creates a mismatch between the declared structure and the actual rendered table, which can confuse users. Second, browsers handle this inconsistently — most will silently clip the span, but the rendered result may not match your intent. Third, if you later add a separate <tbody> or <tfoot> after the current group, the clipped span won’t bridge into it, potentially breaking your layout in unexpected ways.
To fix this, you have two options: reduce the rowspan value to match the number of remaining rows in the row group, or add enough rows to the group so the span fits. You should also check whether your row group boundaries (<thead>, <tbody>, <tfoot>) are placed where you actually intend them.
Note that this same issue applies to colspan exceeding the column count of a row group, though the warning message specifically mentions rowspan clipping to the end of the row group established by a <tbody> (or <thead> / <tfoot>) element.
Examples
Incorrect: rowspan exceeds available rows in <tbody>
This <tbody> has only 2 rows, but the first cell declares rowspan="4":
<table>
<tbody>
<tr>
<td rowspan="4">Spans too far</td>
<td>Row 1</td>
</tr>
<tr>
<td>Row 2</td>
</tr>
</tbody>
</table>
The cell tries to span 4 rows, but only 2 exist in the <tbody>. The browser clips it to 2 rows, and the validator reports the error.
Fixed: Reduce rowspan to match available rows
<table>
<tbody>
<tr>
<td rowspan="2">Spans two rows</td>
<td>Row 1</td>
</tr>
<tr>
<td>Row 2</td>
</tr>
</tbody>
</table>
Fixed: Add rows to accommodate the span
If you actually need the cell to span 4 rows, add the missing rows:
<table>
<tbody>
<tr>
<td rowspan="4">Spans four rows</td>
<td>Row 1</td>
</tr>
<tr>
<td>Row 2</td>
</tr>
<tr>
<td>Row 3</td>
</tr>
<tr>
<td>Row 4</td>
</tr>
</tbody>
</table>
Incorrect: rowspan crosses a row group boundary
This is a common source of the error — a span in one <tbody> trying to reach into the next:
<table>
<tbody>
<tr>
<td rowspan="3">Tries to cross groups</td>
<td>Group 1, Row 1</td>
</tr>
<tr>
<td>Group 1, Row 2</td>
</tr>
</tbody>
<tbody>
<tr>
<td>Group 2, Row 1</td>
</tr>
</tbody>
</table>
The cell with rowspan="3" cannot span from the first <tbody> (2 rows) into the second <tbody>. Each group is independent.
Fixed: Merge the row groups or adjust the span
If the rows logically belong together, combine them into a single <tbody>:
<table>
<tbody>
<tr>
<td rowspan="3">Spans all three rows</td>
<td>Row 1</td>
</tr>
<tr>
<td>Row 2</td>
</tr>
<tr>
<td>Row 3</td>
</tr>
</tbody>
</table>
When browsers build a table’s internal grid, each cell occupies one or more column slots. A cell with colspan="3" occupies three column slots, and columns are established based on the maximum number of slots any row uses. The validator checks that every column position in this grid has at least one cell whose starting position is in that column. If a column exists only because other cells span across it — but no cell ever begins there — the validator raises this warning.
This is a problem for several reasons:
- Accessibility: Screen readers use the table grid model to navigate cells and announce column headers. An empty or orphaned column confuses this navigation, making the table harder to understand for assistive technology users.
- Standards compliance: The HTML specification defines a precise table model. A column with no originating cell suggests a structural error in the table’s markup.
- Rendering inconsistencies: Different browsers may handle these orphaned columns differently, leading to unpredictable layouts.
The most common causes are:
- Excessive colspan values — a cell spans more columns than intended, creating extra columns that other rows don’t fill.
- Missing cells in a row — a row has fewer cells than the table’s column count, leaving trailing columns empty.
- Incorrect rowspan calculations — a cell spans rows, but subsequent rows still include cells for that column position, pushing other cells into non-existent columns or leaving gaps.
To fix the issue, count the total number of columns your table should have, then verify that every row’s cells (accounting for colspan and active rowspan from previous rows) add up to exactly that number. Ensure each column position has at least one cell starting in it across all rows.
Examples
Incorrect: colspan creates a column no cell begins in
In this example, the first row establishes 3 columns. The second row spans all 3 with colspan="3". The third row only has 2 cells, so column 3 has no cell beginning in it.
<table>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
</tr>
<tr>
<td colspan="3">Team Alpha</td>
</tr>
<tr>
<td>Alice</td>
<td>Developer</td>
<!-- Column 3 has no cell beginning here -->
</tr>
</table>
Fixed: every row accounts for all columns
<table>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
</tr>
<tr>
<td colspan="3">Team Alpha</td>
</tr>
<tr>
<td>Alice</td>
<td>Developer</td>
<td>Active</td>
</tr>
</table>
Incorrect: excessive colspan creates extra columns
Here, the second row’s colspan="4" establishes 4 columns, but no other row has a cell starting in column 4.
<table>
<tr>
<th>Q1</th>
<th>Q2</th>
<th>Q3</th>
</tr>
<tr>
<td colspan="4">Full year summary</td>
</tr>
<tr>
<td>100</td>
<td>200</td>
<td>300</td>
</tr>
</table>
Fixed: colspan matches the actual column count
<table>
<tr>
<th>Q1</th>
<th>Q2</th>
<th>Q3</th>
</tr>
<tr>
<td colspan="3">Full year summary</td>
</tr>
<tr>
<td>100</td>
<td>200</td>
<td>300</td>
</tr>
</table>
Incorrect: rowspan causes a missing cell origin
The rowspan="2" on “Alice” means the second row already has column 1 occupied. But the second row only provides one cell, leaving column 3 without a beginning cell in any row except the header.
<table>
<tr>
<th>Name</th>
<th>Task</th>
<th>Hours</th>
</tr>
<tr>
<td rowspan="2">Alice</td>
<td>Design</td>
<td>8</td>
</tr>
<tr>
<td>Code</td>
<!-- Column 1 is occupied by rowspan; column 3 has no cell -->
</tr>
</table>
Fixed: the spanned row includes the right number of cells
Since rowspan="2" on column 1 carries into the next row, that row only needs cells for columns 2 and 3.
<table>
<tr>
<th>Name</th>
<th>Task</th>
<th>Hours</th>
</tr>
<tr>
<td rowspan="2">Alice</td>
<td>Design</td>
<td>8</td>
</tr>
<tr>
<td>Code</td>
<td>6</td>
</tr>
</table>
A good practice is to sketch out your table grid on paper or in a spreadsheet before writing the HTML. Label each column slot and verify that every row fills all slots — either with a new cell or with a rowspan carry-over from a previous row. This makes it much easier to catch mismatched colspan and rowspan values before they cause validation errors.
The HTML specification requires that table structures be coherent — every column in a table must be “used” by at least one cell that starts in it. When you declare a <th> with colspan="2", you’re telling the browser that the table has (at least) two columns. If the body rows don’t provide cells to fill those columns, the table’s structure becomes inconsistent.
This matters for several reasons. Screen readers and other assistive technologies rely on a well-formed table grid to navigate and announce cell contents. An inconsistent column structure can confuse these tools, leading to a poor experience for users who depend on them. Browsers may also render such tables unpredictably, since they have to guess how to handle the mismatch.
Common causes of this error include:
- A colspan value on a header cell that exceeds the actual number of cells in body rows.
- Empty <tbody>, <thead>, or <tfoot> sections that leave declared columns without any cells.
- Rows with fewer cells than other rows define, leaving some columns unoccupied.
- Copy-paste errors or template issues where rows were deleted but headers were left unchanged.
To fix this, examine your table’s column structure. Count the total number of columns implied by each row (accounting for colspan and rowspan attributes) and make sure every row agrees on the total column count. Every column should have at least one cell beginning in it somewhere in the table.
Examples
Incorrect: colspan creates columns with no matching body cells
Here, the header declares two columns, but the body is empty, so column 2 has no cells beginning in it:
<table>
<thead>
<tr>
<th colspan="2">The table header</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
Incorrect: Body rows have fewer cells than the header defines
The header spans three columns, but the body rows only have two cells each. Column 3 has no cells beginning in it:
<table>
<thead>
<tr>
<th colspan="3">Wide header</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</tbody>
</table>
Correct: Body cells match the columns defined by the header
The header spans two columns, and the body row provides exactly two cells:
<table>
<thead>
<tr>
<th colspan="2">The table header</th>
</tr>
</thead>
<tbody>
<tr>
<td>The table body</td>
<td>with two columns</td>
</tr>
</tbody>
</table>
Correct: Three-column header with matching body cells
<table>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>Developer</td>
<td>Active</td>
</tr>
</tbody>
</table>
Correct: Using colspan in body rows to fill all columns
If you need fewer visible cells in a body row, use colspan to span the remaining columns rather than leaving them empty:
<table>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td colspan="2">Info unavailable</td>
</tr>
</tbody>
</table>
Correct: Reducing colspan to match actual content
If the header doesn’t truly need to span multiple columns, simply remove the colspan:
<table>
<thead>
<tr>
<th>The table header</th>
</tr>
</thead>
<tbody>
<tr>
<td>The table body</td>
</tr>
</tbody>
</table>
When a table cell uses colspan to span multiple columns, the HTML specification requires that the columns being spanned actually exist and are accounted for in the table’s column structure. If a cell’s colspan creates columns that no other row has cells beginning in, the validator flags those empty columns. For example, if your widest row has 3 columns but another row contains a cell with colspan="6", columns 4 through 6 are established by that cell but are essentially phantom columns — no cell in any other row starts in them.
This matters for several reasons. Screen readers and assistive technologies rely on a coherent table structure to navigate cells and announce column/row relationships. An inconsistent column count can confuse these tools, leading to a poor experience for users who depend on them. Browsers may render the table without visible errors, but the underlying structure is invalid, which can cause unpredictable layout behavior across different rendering engines.
How to Fix
- Identify the offending row. Look for <td> or <th> elements whose colspan value creates more columns than the rest of the table defines.
- Reduce the colspan value so it matches the actual number of columns in the table.
- Alternatively, add cells to other rows if you genuinely need more columns — make sure every column has at least one cell that begins in it.
A good rule of thumb: the colspan of any cell, combined with its starting column position, should never exceed the total column count of the table.
Examples
Incorrect: colspan exceeds the table’s column count
This table has 2 columns (established by the first row), but the second row’s colspan="5" tries to span 5 columns. Columns 3 through 5 have no cells beginning in them in any row.
<table>
<tr>
<td>First</td>
<td>Second</td>
</tr>
<tr>
<td colspan="5">Spans too many columns</td>
</tr>
</table>
Correct: colspan matches the table’s column count
Set the colspan to 2 so the cell spans exactly the columns that exist.
<table>
<tr>
<td>First</td>
<td>Second</td>
</tr>
<tr>
<td colspan="2">Spans both columns</td>
</tr>
</table>
Incorrect: mixed rows with mismatched column counts
Here the first row establishes 3 columns, but the second row creates a cell starting at column 1 that spans 5 columns, leaving columns 4 and 5 with no cells beginning in them.
<table>
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
</tr>
<tr>
<td colspan="5">Too wide</td>
</tr>
</table>
Correct: expand the table or reduce the span
Option A: Reduce the colspan to match the existing 3 columns.
<table>
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
</tr>
<tr>
<td colspan="3">Spans all three columns</td>
</tr>
</table>
Option B: If you truly need 5 columns, add cells to the other rows so every column has a cell beginning in it.
<table>
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
<td>D</td>
<td>E</td>
</tr>
<tr>
<td colspan="5">Spans all five columns</td>
</tr>
</table>
A td element must be placed within a tr (table row) element, not directly as a child of tbody, thead, tfoot, or table.
The td (table cell) element defines a cell of a table that contains data. According to the HTML standard, td elements must be placed inside tr elements, which are used to group table cells in a row. Directly placing a td element inside tbody, thead, tfoot, or table violates the HTML content model and will cause a validation error.
Incorrect example:
<table>
<tbody>
<td>Cell 1</td>
<td>Cell 2</td>
</tbody>
</table>
Correct example:
<table>
<tbody>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</tbody>
</table>
Always wrap td elements within a tr when constructing table rows.
When you write a <table> without explicitly using <thead> and <tbody>, the HTML parser automatically wraps your <tr> elements in an implicit <tbody>. If any of those rows contain <th> elements intended as column headers, the validator flags them because <th> cells in <tbody> are unexpected — the parser sees header cells appearing in what should be the data body of the table.
While <th> elements are technically valid inside <tbody> (for example, as row headers), this warning usually indicates a structural problem: your column headers aren’t properly separated from your data rows. Properly structuring your table with <thead> and <tbody> matters for several reasons:
- Accessibility: Screen readers use table structure to help users navigate. A <thead> section clearly identifies column headers, making it easier for assistive technology to announce what each data cell represents.
- Styling and behavior: CSS selectors like thead th and tbody td let you target headers and data cells independently. Browsers can also use <thead> and <tbody> to enable scrollable table bodies while keeping headers fixed.
- Standards compliance: Explicitly defining table sections removes ambiguity and ensures consistent parsing across all browsers.
To fix this issue, wrap the row containing your <th> column headers in a <thead> element, and wrap your data rows in a <tbody> element.
Examples
❌ Incorrect: <th> in implicit table body
Here, the parser wraps all rows in an implicit <tbody>, so the <th> elements end up inside the table body:
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Liza</td>
<td>49</td>
</tr>
<tr>
<td>Joe</td>
<td>47</td>
</tr>
</table>
✅ Correct: <th> in explicit <thead>
Wrapping the header row in <thead> and data rows in <tbody> resolves the issue:
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>Liza</td>
<td>49</td>
</tr>
<tr>
<td>Joe</td>
<td>47</td>
</tr>
</tbody>
</table>
✅ Correct: <th> as row headers inside <tbody>
If you intentionally use <th> elements inside <tbody> as row headers, add the scope attribute to clarify their purpose. This is valid and won’t trigger the warning when the table also has a proper <thead>:
<table>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Age</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Liza</th>
<td>49</td>
</tr>
<tr>
<th scope="row">Joe</th>
<td>47</td>
</tr>
</tbody>
</table>
The scope="row" attribute tells assistive technology that these <th> cells are headers for their respective rows, while scope="col" identifies column headers. This combination provides the best accessibility for table data.
The <caption> element is designed to be a brief, descriptive label for its parent <table>. According to the HTML specification, <caption> accepts flow content but explicitly forbids descendant <table> elements. This restriction exists because a table nested inside a caption creates a confusing and semantically meaningless structure — the caption is supposed to describe the table, not contain another one.
Why this is a problem
- Accessibility: Screen readers announce the <caption> as the title of the table. A nested table inside a caption creates a confusing experience for assistive technology users, as the relationship between the tables becomes ambiguous and the caption loses its descriptive purpose.
- Standards compliance: The WHATWG HTML living standard explicitly states that <caption> must have “no <table> element descendants.” Violating this produces a validation error.
- Rendering inconsistencies: Browsers may handle this invalid nesting differently, leading to broken or unpredictable layouts across different environments.
How to fix it
- Remove the table from the caption. The <caption> should contain only text and simple inline elements like <span>, <strong>, <em>, or <a>.
- Place the nested table outside the parent table, either before or after it, or restructure your layout so both tables are siblings.
- If the data in the nested table is genuinely related to the caption’s purpose, consider expressing it as plain text or using a different structural approach entirely.
Examples
❌ Incorrect: A table nested inside a caption
<table>
<caption>
Summary
<table>
<tr>
<td>Extra info</td>
<td>Details</td>
</tr>
</table>
</caption>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
This triggers the validation error because a <table> appears as a descendant of the <caption> element.
✅ Correct: Caption contains only text, tables are separate
<table>
<caption>Summary — Extra info: Details</caption>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
If the extra information truly requires its own table, place it as a sibling:
<table>
<caption>Summary</caption>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alice</td>
<td>95</td>
</tr>
</table>
<table>
<caption>Additional details</caption>
<tr>
<td>Extra info</td>
<td>Details</td>
</tr>
</table>
✅ Correct: Caption with inline formatting only
<table>
<caption>
<strong>Quarterly Results</strong> — <em>All figures in USD</em>
</caption>
<tr>
<th>Quarter</th>
<th>Revenue</th>
</tr>
<tr>
<td>Q1</td>
<td>$1.2M</td>
</tr>
</table>
This is valid because the <caption> contains only text and inline elements (<strong>, <em>), with no <table> descendants.
A td element’s headers attribute must reference the ID of a th element within the same table.
The headers attribute is used to define explicit associations between data cells (td) and header cells (th) by referencing header cell IDs. For valid markup, every ID referenced in the headers attribute must exist as a th element’s id within the same table. This helps users—especially those using assistive technology—understand which header(s) apply to each data cell.
For example, if a td references headers="table_header_1", there must be a th id="table_header_1" present. If it’s missing or mistyped, you’ll get the validator error you described.
Correct pattern:
<table>
<tr>
<th id="table_header_1">Header 1</th>
<th id="table_header_2">Header 2</th>
</tr>
<tr>
<td headers="table_header_1">Row 2, Column 1</td>
<td headers="table_header_2">Row 2, Column 2</td>
</tr>
</table>
Incorrect pattern (missing th ID):
<table>
<tr>
<th>Header 1</th>
<th id="table_header_2">Header 2</th>
</tr>
<tr>
<td headers="table_header_1">Row 2, Column 1</td>
<td headers="table_header_2">Row 2, Column 2</td>
</tr>
</table>
To resolve the error, ensure every value in a td‘s headers attribute matches the id of a th element in the same table.
The summary attribute was used in HTML 4 to provide a text description of a table’s structure and purpose, primarily for screen reader users. In HTML5, this attribute was deprecated because it was invisible to sighted users, creating an unequal experience. It was also frequently misused — authors often duplicated the table’s caption or provided unhelpful descriptions, diminishing its accessibility value.
The HTML Living Standard offers several better alternatives, each suited to different situations:
- Use a <caption> element — Best for a concise title or description that benefits all users, not just screen reader users. The <caption> must be the first child of the <table> element.
- Use a <figure> with <figcaption> — Ideal when you want to provide a longer description or contextual information alongside the table. This approach also semantically groups the table with its description.
- Simplify the table — If your table is straightforward with clear headers, it may not need any additional description at all. Well-structured <th> elements with appropriate scope attributes often provide enough context.
From an accessibility standpoint, the <caption> and <figcaption> approaches are superior because they are visible to all users and part of the document flow. Screen readers announce <caption> content when a user navigates to a table, providing the same benefit the summary attribute once offered — but now everyone can see it.
Examples
❌ Obsolete: Using the summary attribute
This triggers the validation warning because summary is no longer a valid attribute on <table>.
<table summary="This table shows monthly sales figures for 2024.">
<tr>
<th>Month</th>
<th>Sales</th>
</tr>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</table>
✅ Fix 1: Using a <caption> element
Replace the summary attribute with a <caption> as the first child of the <table>. This is the most common and straightforward fix.
<table>
<caption>Monthly sales figures for 2024</caption>
<tr>
<th>Month</th>
<th>Sales</th>
</tr>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</table>
✅ Fix 2: Using <figure> and <figcaption>
This approach is useful when you want to provide a longer description or when the table is referenced as a figure within surrounding content.
<figure>
<figcaption>
Monthly sales figures for 2024, showing a steady increase in revenue
during the first quarter.
</figcaption>
<table>
<tr>
<th>Month</th>
<th>Sales</th>
</tr>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</table>
</figure>
✅ Fix 3: Simplify and rely on clear headers
For simple tables where the data is self-explanatory, well-labeled headers with scope attributes may be sufficient. No extra description is needed.
<table>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Sales</th>
</tr>
</thead>
<tbody>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</tbody>
</table>
You can also combine approaches — use a <caption> for a brief title and wrap the table in a <figure> with a <figcaption> for additional context. The key takeaway is to remove the summary attribute and use visible, semantic HTML elements to describe your table instead.
In earlier versions of HTML (HTML 4 and XHTML 1.0), the width attribute was a standard way to control the dimensions of tables and their cells. HTML5 marked this attribute as obsolete on table-related elements, meaning browsers may still render it, but it is no longer conforming HTML. The W3C validator will report a warning or error whenever it encounters this usage.
This matters for several reasons. First, using obsolete attributes means your markup doesn’t conform to the HTML living standard, which can cause validation failures that obscure more critical issues. Second, relying on presentational HTML attributes mixes content with presentation, making your code harder to maintain. CSS provides far more flexibility and control — you can use relative units, media queries for responsive layouts, and centralized stylesheets that apply consistently across your site. Third, while current browsers still support the obsolete width attribute, future browser versions are not guaranteed to do so.
The fix is straightforward: remove the width attribute and apply the equivalent sizing with CSS. You can use inline styles for quick fixes, but a class-based or external stylesheet approach is generally preferred for maintainability.
Examples
❌ Incorrect: using the obsolete width attribute
<table width="600">
<tr>
<td width="200">Name</td>
<td width="400">Description</td>
</tr>
</table>
This triggers the validator message: The “width” attribute on the “table” element is obsolete. Use CSS instead. — and the same for each <td>.
✅ Fixed: using inline CSS
<table style="width: 600px;">
<tr>
<td style="width: 200px;">Name</td>
<td style="width: 400px;">Description</td>
</tr>
</table>
✅ Better: using CSS classes
<style>
.data-table {
width: 100%;
max-width: 600px;
}
.data-table .col-name {
width: 33%;
}
.data-table .col-desc {
width: 67%;
}
</style>
<table class="data-table">
<tr>
<td class="col-name">Name</td>
<td class="col-desc">Description</td>
</tr>
</table>
Using classes keeps your HTML clean and makes it easy to adjust sizing in one place. Percentage-based widths also adapt better to different screen sizes.
❌ Incorrect: width on <col> and <colgroup>
<table>
<colgroup width="100">
<col width="50">
<col width="50">
</colgroup>
<tr>
<td>A</td>
<td>B</td>
</tr>
</table>
✅ Fixed: CSS on <col> elements
<table>
<colgroup>
<col style="width: 50px;">
<col style="width: 50px;">
</colgroup>
<tr>
<td>A</td>
<td>B</td>
</tr>
</table>
❌ Incorrect: width on <th>
<table>
<tr>
<th width="150">Header</th>
</tr>
</table>
✅ Fixed
<table>
<tr>
<th style="width: 150px;">Header</th>
</tr>
</table>
Tips for migrating
- Search your codebase for width= inside table-related tags. A regex like <(table|td|th|col|colgroup)[^>]+width= can help locate all instances.
- Convert pixel values directly — a width="200" attribute is equivalent to width: 200px in CSS.
- Consider responsive design — this is a good opportunity to switch from fixed pixel widths to percentages, em units, or other flexible values.
- Use table-layout: fixed on the <table> element if you need columns to respect exact widths rather than auto-sizing based on content.
Ready to validate your sites?
Start your free trial today.