HTML Guides for tr
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.
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>
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>
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>
A <tr> element already has the ARIA role row, so adding role="row" to it repeats what browsers and screen readers already know.
HTML gives each element a default ARIA role. A <tr> inside a <table> is exposed to assistive technology as a row with no extra markup, and the cells inside it get their roles the same way. Writing role="row" by hand changes nothing for users and makes the markup harder to read.
The same attribute also triggers an error. When the <table> has no role attribute, or has role="table", role="grid" or role="treegrid", its <tr> elements can't have a role attribute at all. Removing role="row" fixes both messages.
This often shows up in tables pasted from online word processors, which add role="row" to every row. The row role is meant for tables built from generic elements such as <div>, where nothing else tells a screen reader which elements form a row. A native <tr> doesn't need it.
Invalid example
<table>
<tr role="row">
<th>Product</th>
<th>Price</th>
</tr>
<tr role="row">
<td>Notebook</td>
<td>$4.50</td>
</tr>
</table>
Valid example
<table>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
<tr>
<td>Notebook</td>
<td>$4.50</td>
</tr>
</table>
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