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 acolspanattribute. - Extra cells in later rows — A row further down the table has more cells than intended.
- Mismatched
colspanarithmetic — The sum of cells and theircolspanvalues 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.
A row in this table has more cells than its <colgroup> and <col> elements declare, so some of its cells sit past the last column the markup defines.
When a table has column markup, that markup sets the column count before any row is read. Each <col> adds one column, or as many as its span attribute says. A <colgroup> with no <col> inside adds the number in its own span, which defaults to 1. A row is X columns wide when its cells add up to X, with each cell counting as many columns as its colspan. The validator reports this error once for every row wider than the Y columns the markup declares.
A common cause is an empty <colgroup></colgroup> that a table editor or template writes into every table. It still counts as one column, so every row of a table with two or more columns triggers the error. Another cause is a column added to the rows but not to the <colgroup>. Browsers still show the extra cells, but styles set on <col>, such as width or background-color, don't reach the columns the markup leaves out.
To fix it, declare every column the rows use: add the missing <col> elements, raise the span, or remove a <colgroup> that doesn't style anything.
Invalid example
The <colgroup> declares one column, but each row has two cells:
<table>
<colgroup>
<col class="item">
</colgroup>
<tr>
<th>Item</th>
<th>Price</th>
</tr>
<tr>
<td>Coffee</td>
<td>2.50</td>
</tr>
</table>
Valid example
One <col> for each column in the rows:
<table>
<colgroup>
<col class="item">
<col class="price">
</colgroup>
<tr>
<th>Item</th>
<th>Price</th>
</tr>
<tr>
<td>Coffee</td>
<td>2.50</td>
</tr>
</table>
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 thecolspanattribute 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>
An element with role="row" must sit inside an element whose role is table, grid, treegrid, or rowgroup, or be claimed by one through aria-owns. A row on its own has no structure to belong to, so assistive technologies cannot tell which table or grid it is part of.
The row role describes one row of a composite structure. Those four allowed owners fall into two groups: table and rowgroup for static tabular data, and grid and treegrid for interactive widgets that support keyboard navigation. In every case the container supplies the context a screen reader needs to announce the row's position and its cells. Placed outside such a container, the row is orphaned and the whole table semantics break down.
The most reliable fix is to nest the role="row" element inside its container in the DOM. When your layout prevents that, keep the row where it is and point aria-owns from the container at the row's id.
Invalid example
The role="row" is a sibling of the table, so nothing contains or owns it:
<div role="table" aria-label="Prices"></div>
<div role="row">
<span role="cell">Basic</span>
<span role="cell">$9</span>
</div>
Valid example
Nest the row inside the element with role="table":
<div role="table" aria-label="Prices">
<div role="row">
<span role="cell">Basic</span>
<span role="cell">$9</span>
</div>
</div>
If the DOM nesting is not possible, leave the row in place and add aria-owns="price-row" to the container so assistive technologies still treat it as the owner.
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 dir attribute on <table> only accepts ltr, rtl or auto, so a value such as none is invalid.
dir sets the base direction of the table. It affects the text inside each cell and also the column order: with rtl the first column sits on the right edge of the table, with ltr on the left. auto lets the browser pick the direction from the first strongly directional character in the content.
HTML has no keyword that switches direction off. A table without dir inherits the direction of its parent, usually set on the <html> element, which is probably what none was meant to do. Remove the attribute, or set ltr or rtl when the table needs a different direction from the rest of the page. An empty dir="" is invalid for the same reason.
Invalid example
<table dir="none">
<tr>
<th>Size</th>
<th>Price</th>
</tr>
<tr>
<td>Small</td>
<td>4.50</td>
</tr>
</table>
Valid example
<table>
<tr>
<th>Size</th>
<th>Price</th>
</tr>
<tr>
<td>Small</td>
<td>4.50</td>
</tr>
</table>
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 userowspanvalues 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
rowspanvalues. If previous cells span too many rows, reduce theirrowspanso 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, orborder-spacinginstead.
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
paddingto cells or use theborder-spacingproperty 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 table can only hold another table inside one of its cells, so a <table> start tag placed directly inside an open table, outside any <td> or <th>, is an error.
Between <table> and </table>, HTML allows only table parts: <caption>, <colgroup>, <thead>, <tbody>, <tfoot> and <tr>. Content, a nested table included, goes in a <td> or <th> cell. When the parser meets a new <table> tag anywhere else in an open table, it closes the open table at that point and starts the new one after it. The two tables end up as siblings in the DOM, and the </table> written for the outer table has nothing left to close, so the validator also reports a stray end tag.
Two mistakes usually cause this. One is a missing </table>: a table never gets closed, and the next table on the page starts inside it. The other is a wrapper table with another table directly inside it and no <tr> or <td> in between, such as one left behind by a WYSIWYG editor.
Close the first table before the next one starts, or remove the extra wrapper. If you do want one table inside another, put the inner table in a <td> of the outer one. To center a table or add space around it, use CSS on the table itself instead of wrapping it in another table.
Invalid example
<table>
<table>
<tr>
<th>Day</th>
<th>Opening hours</th>
</tr>
<tr>
<td>Monday</td>
<td>9:00 to 17:00</td>
</tr>
</table>
</table>
Valid example
<table>
<tr>
<th>Day</th>
<th>Opening hours</th>
</tr>
<tr>
<td>Monday</td>
<td>9:00 to 17:00</td>
</tr>
</table>
A <form> can't sit directly inside a <table>, <thead>, <tbody>, <tfoot> or <tr>: wrap the whole table in the form, or put the form inside a single <td> or <th> cell.
Table elements only accept specific children. A <table> holds <caption>, <colgroup>, <thead>, <tbody>, <tfoot> and <tr>, and a <tr> holds <td> and <th>. A <form> wrapped around one or more rows breaks that structure.
The browser's parser handles this case in a special way. It creates the form as an empty element at the spot where the tag appears, closes it right away, and drops the matching </form> end tag, which the validator reports as a separate Stray end tag “form” error. Browsers still link the fields in the following rows to that empty form, so submitting often appears to work. But the fields are not inside the form in the DOM: a selector like form input won't find them, and if a script moves or re-renders those rows, the fields lose their form.
When each row needs its own form, such as an "Update" button per cart item, keep the <form> elements outside the table and link each field and button to its form with the form attribute, which takes the form's id.
Invalid example
<table>
<form action="/cart" method="post">
<tr>
<td><label for="qty">Quantity</label></td>
<td><input type="number" id="qty" name="qty"></td>
<td><button type="submit">Update</button></td>
</tr>
</form>
</table>
Valid example
<form action="/cart" method="post">
<table>
<tr>
<td><label for="qty">Quantity</label></td>
<td><input type="number" id="qty" name="qty"></td>
<td><button type="submit">Update</button></td>
</tr>
</table>
</form>
Valid example with one form per row
<form id="item-1" action="/cart/1" method="post"></form>
<form id="item-2" action="/cart/2" method="post"></form>
<table>
<tr>
<td><input type="number" name="qty" form="item-1" aria-label="Quantity for item 1"></td>
<td><button type="submit" form="item-1">Update</button></td>
</tr>
<tr>
<td><input type="number" name="qty" form="item-2" aria-label="Quantity for item 2"></td>
<td><button type="submit" form="item-2">Update</button></td>
</tr>
</table>
An <input> can't sit directly inside a <table>, <thead>, <tbody>, <tfoot> or <tr>: put it inside a <td> or <th> cell, or move it outside the table.
Table elements only accept specific children. A <table> holds <caption>, <colgroup>, <thead>, <tbody>, <tfoot> and <tr>, and a <tr> holds <td> and <th>.
When the browser's parser finds a misplaced <input>, it moves the element out and inserts it just before the table. The HTML specification calls this foster parenting. The field then shows up above the table instead of in the row where you wrote it. Hidden inputs (type="hidden") are the exception: the parser leaves them in place, but the validator flags them too.
This usually comes from forms laid out with tables, where hidden fields such as CSRF tokens or page numbers end up between <tr> elements. Move hidden inputs before or after the <table>, keeping them inside the <form>, and put visible inputs in a cell. If the <form> tag itself sits inside the table, move it out so it wraps the whole <table>. A <form> between rows triggers its own validation error.
Invalid example
<form action="/search">
<table>
<tr>
<td><label for="q">Search</label></td>
<td><input type="text" id="q" name="q"></td>
</tr>
<input type="hidden" name="page" value="1">
</table>
</form>
Valid example
<form action="/search">
<input type="hidden" name="page" value="1">
<table>
<tr>
<td><label for="q">Search</label></td>
<td><input type="text" id="q" name="q"></td>
</tr>
</table>
</form>
A <p> can't sit directly inside a <table>, <thead>, <tbody>, <tfoot> or <tr>: put the paragraph inside a <td> or <th> cell, or move it outside the table.
Table elements only accept specific children. A <table> holds <caption>, <colgroup>, <thead>, <tbody>, <tfoot> and <tr>, and a <tr> holds <td> and <th>. A paragraph is allowed inside a cell or a <caption>, but not between rows.
When the browser's parser finds a misplaced <p>, it moves the paragraph out and inserts it just before the table. The HTML specification calls this foster parenting. A paragraph you wrote as a table title or a note between rows shows up above the table instead, and an empty <p></p> left between rows becomes an empty paragraph above it.
The validator stops checking the page at this error and adds Cannot recover after last error. Any further errors will be ignored. right after it. Nothing below that point gets checked, so new issues further down the page can show up once you fix this one.
To fix it, put a table title in a <caption>, move text that belongs to a row into a <td> or <th>, and delete empty <p></p> pairs between rows. If those empty paragraphs were there to add space between rows, use CSS padding on the cells or border-spacing on the table instead.
Invalid example
<table>
<p>Opening hours</p>
<tr>
<td>Monday</td>
<td>9:00 to 17:00</td>
</tr>
<p></p>
<tr>
<td>Saturday</td>
<td>Closed</td>
</tr>
</table>
Valid example
<table>
<caption>Opening hours</caption>
<tr>
<td>Monday</td>
<td>9:00 to 17:00</td>
</tr>
<tr>
<td>Saturday</td>
<td>Closed</td>
</tr>
</table>
The HTML specification defines a strict nesting hierarchy for table elements. A <td> (table data cell) must be a direct child of a <tr> (table row), and that <tr> must in turn live inside a <table>, <thead>, <tbody>, or <tfoot>. When the browser's parser encounters a <td> in an unexpected location, it considers it a "stray" start tag — meaning the element has no valid place in the current parsing context.
This error commonly occurs in several scenarios:
- A
<td>is placed directly inside a<table>without a wrapping<tr>. - A
<td>is placed directly inside<thead>,<tbody>, or<tfoot>without a<tr>. - A
<td>appears completely outside of any table structure, such as inside a<div>or the document<body>. - A
<tr>tag was accidentally deleted or misspelled during editing, leaving its<td>children orphaned.
Why this matters
Browser inconsistency: Browsers use error-recovery algorithms to handle invalid markup, but they may handle stray table cells differently. Some browsers might silently insert an implicit <tr>, while others might move the content outside the table entirely. This leads to unpredictable rendering across browsers.
Accessibility: Screen readers and assistive technologies rely on proper table semantics to navigate and announce cell content. A stray <td> breaks the logical row-and-column relationship, making the data difficult or impossible for assistive technology users to interpret correctly.
Maintainability: Invalid nesting makes your markup harder to understand, debug, and style with CSS. Table-specific CSS selectors and properties may not behave as expected when the DOM structure is malformed.
How to fix it
- Locate every
<td>element flagged by the validator. - Ensure it is a direct child of a
<tr>element. - Ensure that
<tr>is itself inside a<table>,<thead>,<tbody>, or<tfoot>. - If the
<td>was outside a table entirely, either wrap it in a full table structure or replace it with a more appropriate element like a<div>or<span>.
Examples
<td> directly inside <table> without <tr>
This is the most common cause. The <td> elements are children of <table> but lack a wrapping <tr>:
<!-- ❌ Bad: td is a direct child of table -->
<table>
<td>Name</td>
<td>Email</td>
</table>
Wrap the cells in a <tr>:
<!-- ✅ Good: td is inside a tr -->
<table>
<tr>
<td>Name</td>
<td>Email</td>
</tr>
</table>
<td> directly inside <tbody> without <tr>
<!-- ❌ Bad: td is a direct child of tbody -->
<table>
<tbody>
<td>Alice</td>
<td>alice@example.com</td>
</tbody>
</table>
<!-- ✅ Good: td is wrapped in a tr inside tbody -->
<table>
<tbody>
<tr>
<td>Alice</td>
<td>alice@example.com</td>
</tr>
</tbody>
</table>
<td> used outside any table context
Sometimes a <td> ends up outside a table entirely, perhaps from a copy-paste error or a templating mistake:
<!-- ❌ Bad: td has no table ancestor -->
<div>
<td>Some content</td>
</div>
If you intended to display content in a non-tabular layout, use an appropriate element instead:
<!-- ✅ Good: use a span or div for non-tabular content -->
<div>
<span>Some content</span>
</div>
Missing or misspelled <tr> tag
A typo can leave <td> elements orphaned:
<!-- ❌ Bad: opening tr tag is misspelled -->
<table>
<t>
<td>Price</td>
<td>$9.99</td>
</t>
</table>
<!-- ✅ Good: correct tr element -->
<table>
<tr>
<td>Price</td>
<td>$9.99</td>
</tr>
</table>
Complete table with proper structure
For reference, here is a well-structured table using <thead>, <tbody>, <th>, and <td> — all correctly nested:
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget</td>
<td>$4.99</td>
</tr>
<tr>
<td>Gadget</td>
<td>$14.99</td>
</tr>
</tbody>
</table>
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>
A cell inside a <thead> has a rowspan value larger than the number of rows left in that <thead>.
The <thead>, <tbody> and <tfoot> elements each form a row group, and a cell's rowspan can't reach from one row group into the next. When a header cell asks for more rows than its <thead> holds, the browser stops the cell at the last header row, as if the extra part of the rowspan weren't there.
This usually comes from a two-level header. The first row has a cell such as "Name" with rowspan="2" next to a grouped heading with colspan="2", and the row of sub-headings that belongs under the group ends up after </thead>, as the first row of the <tbody>. The "Name" cell can't extend into the <tbody>, so that row ends up one cell short, and the validator usually adds a second message about it: "A table row was 2 columns wide, which is less than the column count established by the first row (3)."
Put every header row inside the <thead> so the span has rows to cover. If the header really has a single row, remove the rowspan.
Invalid example
<table>
<thead>
<tr>
<th rowspan="2">Name</th>
<th colspan="2">Score</th>
</tr>
</thead>
<tbody>
<tr>
<th>Round 1</th>
<th>Round 2</th>
</tr>
<tr>
<td>Ana</td>
<td>8</td>
<td>9</td>
</tr>
</tbody>
</table>
Valid example
<table>
<thead>
<tr>
<th rowspan="2">Name</th>
<th colspan="2">Score</th>
</tr>
<tr>
<th>Round 1</th>
<th>Round 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ana</td>
<td>8</td>
<td>9</td>
</tr>
</tbody>
</table>
This table's <col> elements declare more columns than its rows fill, so column N has no <td> or <th> starting in it.
Each <col> inside a <colgroup> adds one column to the table, or as many columns as its span attribute says. The HTML table model then requires every column to have at least one cell that begins in it. A cell that only reaches the column through colspan doesn't count: with <col span="3"> and a row of <td>Alice</td><td colspan="2">2019</td>, column 3 is still reported.
The usual cause is column markup that no longer matches the content, such as a template that always writes the same <colgroup>, or a column removed from every row but left in the <colgroup>. Rows shorter than the column markup also trigger a companion error, "A table row was 2 columns wide, which is less than the column count established using column markup (3)".
To fix it, make the column markup match the rows. Remove the extra <col> elements or lower their span, or add the missing cells if that column should hold data.
Invalid example
Three <col> elements, but every row has only two cells:
<table>
<colgroup>
<col class="name">
<col class="role">
<col class="year">
</colgroup>
<tr>
<th>Name</th>
<th>Year</th>
</tr>
<tr>
<td>Alice Moreno</td>
<td>2019</td>
</tr>
</table>
Valid example
The <colgroup> declares the same two columns the rows use:
<table>
<colgroup>
<col class="name">
<col class="year">
</colgroup>
<tr>
<th>Name</th>
<th>Year</th>
</tr>
<tr>
<td>Alice Moreno</td>
<td>2019</td>
</tr>
</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
colspanvalues — 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
rowspancalculations — 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
colspanvalue 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 whosecolspanvalue creates more columns than the rest of the table defines. - Reduce the
colspanvalue 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>
The HTML specification defines a strict content model for tables. The <td> (table data) element represents a single cell containing data, and it can only appear as a child of a <tr> element. Table sectioning elements like <tbody>, <thead>, and <tfoot> may only contain <tr> elements as direct children — not <td> or <th> cells directly. When the validator encounters a <td> start tag inside a table body (or other section) without a wrapping <tr>, it reports this error.
This issue typically arises in a few common scenarios:
- A missing
<tr>wrapper — the developer placed cells directly inside<tbody>or<table>without creating a row. - A prematurely closed
<tr>— a typo or stray</tr>tag ended the row too early, leaving subsequent<td>elements orphaned. - Dynamically generated HTML — template engines or JavaScript may produce table markup where
<tr>elements are accidentally omitted.
Why this matters
- Standards compliance: The HTML living standard explicitly requires
<td>elements to be children of<tr>. Violating this produces invalid markup. - Browser inconsistency: Browsers will attempt to error-correct by implicitly creating
<tr>elements, but different browsers may interpret the malformed structure differently, leading to unpredictable rendering. - Accessibility: Screen readers and assistive technologies rely on correct table structure to navigate rows and columns. Missing
<tr>elements can confuse these tools, making the data harder or impossible to understand for users who depend on them. - Maintainability: Invalid table markup is harder to style with CSS and harder for other developers to understand and maintain.
Examples
Incorrect — <td> directly inside <tbody>
The <td> elements are children of <tbody> instead of being wrapped in a <tr>:
<table>
<tbody>
<td>Name</td>
<td>Age</td>
</tbody>
</table>
Correct — <td> wrapped in <tr>
Adding a <tr> element creates a valid table row:
<table>
<tbody>
<tr>
<td>Name</td>
<td>Age</td>
</tr>
</tbody>
</table>
Incorrect — prematurely closed <tr> leaves orphaned cells
Here the first </tr> closes the row too early, so the second <td> ends up directly inside <tbody>:
<table>
<tbody>
<tr>
<td>Cell 1</td>
</tr>
<td>Cell 2</td>
<td>Cell 3</td>
</tbody>
</table>
Correct — all cells placed inside proper rows
Either include all cells in one row, or create multiple rows as needed:
<table>
<tbody>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
<td>Cell 3</td>
</tr>
</tbody>
</table>
Or, if you intended two separate rows:
<table>
<tbody>
<tr>
<td>Cell 1</td>
</tr>
<tr>
<td>Cell 2</td>
<td>Cell 3</td>
</tr>
</tbody>
</table>
Incorrect — <td> directly inside <table> with no sections or rows
<table>
<td>Alpha</td>
<td>Beta</td>
</table>
Correct — minimal valid table structure
While <tbody> is optional (browsers add it implicitly), a <tr> is always required:
<table>
<tr>
<td>Alpha</td>
<td>Beta</td>
</tr>
</table>
To fix this error, inspect your table markup and ensure every <td> (and <th>) element is a direct child of a <tr>. Check for stray closing </tr> tags that might end rows prematurely, and verify that any dynamically generated table content produces <tr> wrappers around cell elements.
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 thandtbody tdlet 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 border attribute on the <table> element is obsolete in HTML5 and should be replaced with CSS styling.
The border attribute was commonly used in older HTML to add borders to tables. In HTML5, the only valid value for the border attribute on a <table> is an empty string ("") or "1", and even then it's considered obsolete. The validator message mentions img { border: 0; } because the border attribute was also frequently used on <img> elements, but the same principle applies to tables — presentational attributes should be replaced with CSS.
To style table borders, use the CSS border property on the <table>, <th>, and <td> elements. The border-collapse property is also useful for controlling whether borders are merged or separated.
HTML Examples
❌ Invalid: using the obsolete border attribute
<table border="2">
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</table>
✅ Valid: using CSS for borders
<style>
table, td {
border: 2px solid black;
border-collapse: collapse;
}
</style>
<table>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</table>
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.
Validate at scale.
Ship accessible websites, faster.
Automated HTML & accessibility validation for large sites. Check thousands of pages against WCAG guidelines and W3C standards in minutes, not days.
1K Weekly
1,000 monthly checks. Cancel anytime.
Get 1K Weekly →Join teams across 40+ countries