Guías HTML para td
Aprende a identificar y corregir errores comunes de validación HTML marcados por el W3C Validator, para que tus páginas cumplan con los estándares y se muestren correctamente en todos los navegadores. También consulta nuestras Guías de accesibilidad.
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 aria-checked attribute communicates the checked state of an interactive widget to assistive technologies. According to the WAI-ARIA specification, this attribute is only permitted on elements that have a role supporting the “checked” state — such as checkbox, switch, radio, menuitemcheckbox, or menuitemradio. A plain <td> element has an implicit role of cell (or gridcell when inside a role="grid" table), neither of which supports aria-checked. When the validator encounters aria-checked on a <td> without a compatible role, it flags the element as invalid.
This matters for several reasons:
- Accessibility: Screen readers and other assistive technologies rely on the relationship between role and ARIA state attributes. An aria-checked on an element without a recognized checkable role creates a confusing or broken experience — users may not understand that the cell is supposed to be interactive.
- Standards compliance: The ARIA in HTML specification defines strict rules about which attributes are allowed on which roles. Violating these rules means your HTML is technically invalid.
- Browser behavior: Browsers may ignore aria-checked entirely when it’s used on an element without a valid role, making the attribute useless.
How to fix it
You have two main approaches depending on what your <td> is meant to do:
1. Add an appropriate role attribute. If the table cell genuinely represents a checkable control (for example, in an interactive data grid), add role="checkbox", role="switch", or another appropriate checkable role to the <td>, along with tabindex for keyboard accessibility.
2. Remove aria-checked and use a real control. If the cell simply contains a checkbox or toggle, place an actual <input type="checkbox"> inside the <td> and remove the ARIA attributes from the cell itself. Native HTML controls already communicate their state to assistive technologies without extra ARIA.
Examples
❌ Incorrect: aria-checked without a role
<table>
<tr>
<td aria-checked="true">Selected</td>
<td>Item A</td>
</tr>
</table>
This triggers the error because <td> has the implicit role of cell, which does not support aria-checked.
✅ Fix: Add a compatible role to the <td>
<table role="grid">
<tr>
<td role="checkbox" aria-checked="true" tabindex="0">Selected</td>
<td>Item A</td>
</tr>
</table>
Here the <td> explicitly has role="checkbox", which supports aria-checked. The tabindex="0" makes it keyboard-focusable, and role="grid" on the table signals that cells may be interactive.
✅ Fix: Use a native checkbox inside the <td>
<table>
<tr>
<td>
<label>
<input type="checkbox" checked>
Selected
</label>
</td>
<td>Item A</td>
</tr>
</table>
This approach is often the best option. The native <input type="checkbox"> already conveys its checked state to assistive technologies, and no ARIA attributes are needed on the <td>.
❌ Incorrect: Mismatched role and aria-checked
<table>
<tr>
<td role="button" aria-checked="false">Toggle</td>
<td>Item B</td>
</tr>
</table>
The button role does not support aria-checked. This would trigger a different but related validation error.
✅ Fix: Use a role that supports aria-checked
<table role="grid">
<tr>
<td role="switch" aria-checked="false" tabindex="0">Toggle</td>
<td>Item B</td>
</tr>
</table>
The switch role supports aria-checked and is appropriate for toggle-style controls.
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>
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.
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>
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.
In earlier versions of HTML, the align attribute was used directly on <td> (and other table elements like <tr> and <th>) to control horizontal alignment of cell content. Values like left, center, right, and justify were common. HTML5 made this attribute obsolete in favor of CSS, which provides a cleaner separation of content and presentation.
While most browsers still honor the align attribute for backward compatibility, relying on it is discouraged. It violates modern web standards, mixes presentational concerns into your markup, and makes styling harder to maintain. CSS offers far more flexibility — you can target cells by class, use responsive styles, or change alignment through media queries without touching your HTML.
How to fix it
- Remove the align attribute from the <td> element.
- Apply the CSS text-align property instead, either via an inline style attribute, a <style> block, or an external stylesheet.
The CSS text-align property accepts the same values the old attribute did: left, center, right, and justify.
For vertical alignment, the obsolete valign attribute should similarly be replaced with the CSS vertical-align property.
Examples
❌ Obsolete: using the align attribute
<table>
<tr>
<td align="center">Centered content</td>
<td align="right">Right-aligned content</td>
</tr>
</table>
✅ Fixed: using inline CSS
<table>
<tr>
<td style="text-align: center;">Centered content</td>
<td style="text-align: right;">Right-aligned content</td>
</tr>
</table>
✅ Fixed: using a stylesheet (recommended)
Using classes keeps your HTML clean and makes it easy to update styles across your entire site.
<style>
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
</style>
<table>
<tr>
<td class="text-center">Centered content</td>
<td class="text-right">Right-aligned content</td>
</tr>
</table>
✅ Fixed: applying alignment to an entire column
If every cell in a column needs the same alignment, you can target cells by position instead of adding a class to each one.
<style>
td:nth-child(2) {
text-align: right;
}
</style>
<table>
<tr>
<td>Item</td>
<td>$9.99</td>
</tr>
<tr>
<td>Another item</td>
<td>$14.50</td>
</tr>
</table>
This same approach applies to <th> elements and the <tr> element, which also had an obsolete align attribute in older HTML. In all cases, replace the attribute with the CSS text-align property.
The headers attribute creates explicit associations between data cells (td) and header cells (th) in complex tables. This is especially important for tables with irregular structures—such as those with merged cells or multiple header levels—where the browser cannot automatically determine which headers apply to which data cells.
When the validator reports this error, it means one or more IDs referenced in a td‘s headers attribute cannot be matched to any th element with that id in the same table. Common causes include:
- Typos — A small misspelling in either the headers value or the th element’s id.
- Missing id — The th element exists but doesn’t have an id attribute assigned.
- Removed or renamed headers — The th was deleted or its id was changed during refactoring, but the td still references the old value.
- Cross-table references — The th with the referenced id exists in a different <table>, which is not allowed.
Why this matters
This issue directly impacts accessibility. Screen readers use the headers attribute to announce which header cells are associated with a data cell. When a referenced ID doesn’t resolve to a th in the same table, assistive technology cannot provide this context, making the table confusing or unusable for users who rely on it. Broken headers references also indicate invalid HTML according to the WHATWG HTML specification, which requires that each token in the headers attribute match the id of a th cell in the same table.
How to fix it
- Locate the td element flagged by the validator and note the ID it references.
- Search the same <table> for a th element with a matching id.
- If the th exists but has no id or a different id, add or correct the id attribute so it matches.
- If the th was removed, either restore it or remove the headers attribute from the td.
- Double-check for case sensitivity — HTML id values are case-sensitive, so headers="Name" does not match id="name".
Examples
Incorrect: headers references a non-existent ID
The first td references "product", but no th has id="product". The second th has id="cost", but the second td references "price" — a mismatch.
<table>
<tr>
<th>Product</th>
<th id="cost">Price</th>
</tr>
<tr>
<td headers="product">Widget</td>
<td headers="price">$9.99</td>
</tr>
</table>
Correct: each headers value matches a th with the same id
<table>
<tr>
<th id="product">Product</th>
<th id="cost">Price</th>
</tr>
<tr>
<td headers="product">Widget</td>
<td headers="cost">$9.99</td>
</tr>
</table>
Correct: multiple headers on a single td
In complex tables, a data cell may relate to more than one header. List multiple IDs separated by spaces — each one must correspond to a th in the same table.
<table>
<tr>
<th id="region" rowspan="2">Region</th>
<th id="q1" colspan="2">Q1</th>
</tr>
<tr>
<th id="sales">Sales</th>
<th id="returns">Returns</th>
</tr>
<tr>
<td headers="region">North</td>
<td headers="q1 sales">1200</td>
<td headers="q1 returns">45</td>
</tr>
</table>
Tip: simple tables may not need headers at all
For straightforward tables with a single row of column headers, browsers and screen readers can infer the associations automatically. In those cases, you can omit the headers attribute entirely and avoid this class of error:
<table>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
<tr>
<td>Widget</td>
<td>$9.99</td>
</tr>
</table>
Reserve the headers attribute for complex tables where automatic association is insufficient — such as tables with cells that span multiple rows or columns, or tables with headers in both rows and columns.
The scope attribute tells browsers and assistive technologies how a header cell relates to the data cells around it. Its valid values are col, row, colgroup, and rowgroup. In older versions of HTML, scope was permitted on <td> elements, but the current HTML Living Standard restricts it to <th> elements only. When the W3C validator encounters scope on a <td>, it flags it as obsolete.
This matters for several reasons. First, if a cell acts as a header for other cells, it should be marked up as a <th>, not a <td>. Using <td scope="row"> sends conflicting signals — the element says “I’m a data cell” while the attribute says “I’m a header for this row.” Second, screen readers rely on proper <th> elements with scope to announce table relationships. A <td> with scope may not be interpreted correctly, making the table harder to navigate for users of assistive technology. Third, using obsolete attributes means your markup doesn’t conform to current standards, which could lead to unpredictable behavior in future browsers.
The fix is straightforward: if a cell has a scope attribute, it’s acting as a header and should be a <th> element. Change the <td> to <th> and keep the scope attribute. If the cell is genuinely a data cell and not a header, remove the scope attribute entirely and leave it as a <td>.
Examples
Incorrect: scope on a <td> element
<table>
<tr>
<td scope="col">Name</td>
<td scope="col">Role</td>
</tr>
<tr>
<td scope="row">Alice</td>
<td>Engineer</td>
</tr>
</table>
This triggers the validation error because scope is used on <td> elements. The first row contains column headers and the first column contains row headers, yet they are all marked as data cells.
Correct: scope on <th> elements
<table>
<tr>
<th scope="col">Name</th>
<th scope="col">Role</th>
</tr>
<tr>
<th scope="row">Alice</th>
<td>Engineer</td>
</tr>
</table>
Now the header cells are correctly marked with <th>, and the scope attribute is valid on each one. Screen readers can properly associate “Alice” with “Engineer” and announce the column header “Role” when navigating to that cell.
A more complete table example
<table>
<thead>
<tr>
<th scope="col">Day</th>
<th scope="col">Morning</th>
<th scope="col">Afternoon</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Monday</th>
<td>Meeting</td>
<td>Code review</td>
</tr>
<tr>
<th scope="row">Tuesday</th>
<td>Workshop</td>
<td>Planning</td>
</tr>
</tbody>
</table>
Here, scope="col" on the column headers in <thead> tells assistive technology that “Day,” “Morning,” and “Afternoon” each apply to the cells below them. scope="row" on “Monday” and “Tuesday” indicates they apply to the cells in their respective rows. Every scope attribute sits on a <th>, so the markup is valid and accessible.
When to remove scope instead
If the cell truly contains data and isn’t a header, simply remove the scope attribute:
<!-- Before (invalid) -->
<td scope="row">Some data</td>
<!-- After (valid) -->
<td>Some data</td>
Only add scope when a cell genuinely serves as a header. If it does, make it a <th>. If it doesn’t, leave it as a plain <td> without scope.
The valign attribute was part of earlier HTML specifications (HTML 4.01 and XHTML 1.0) and accepted values like top, middle, bottom, and baseline to control how content was vertically positioned within a table cell. In HTML5, this attribute is obsolete because the specification separates content structure from presentation. All visual styling should be handled through CSS.
This matters for several reasons. First, using obsolete attributes triggers W3C validation errors, which can indicate broader code quality issues. Second, browsers may eventually drop support for legacy presentational attributes, potentially breaking your layout. Third, CSS provides far more flexibility and maintainability — you can style entire tables or groups of cells with a single rule instead of repeating valign on every <td>.
The valign attribute was also valid on <th>, <tr>, <thead>, <tbody>, and <tfoot> elements, and it is obsolete on all of them. The fix is the same: use the CSS vertical-align property.
To fix this issue, remove the valign attribute from your <td> elements and apply the equivalent CSS vertical-align property. The CSS property accepts the same familiar values: top, middle, bottom, and baseline.
Examples
❌ Incorrect: using the obsolete valign attribute
<table>
<tr>
<td valign="top">Top-aligned content</td>
<td valign="middle">Middle-aligned content</td>
<td valign="bottom">Bottom-aligned content</td>
</tr>
</table>
✅ Fixed: using inline CSS
<table>
<tr>
<td style="vertical-align: top;">Top-aligned content</td>
<td style="vertical-align: middle;">Middle-aligned content</td>
<td style="vertical-align: bottom;">Bottom-aligned content</td>
</tr>
</table>
✅ Fixed: using a class-based approach (recommended)
For better maintainability, define reusable CSS classes instead of repeating inline styles:
<style>
.valign-top { vertical-align: top; }
.valign-middle { vertical-align: middle; }
.valign-bottom { vertical-align: bottom; }
</style>
<table>
<tr>
<td class="valign-top">Top-aligned content</td>
<td class="valign-middle">Middle-aligned content</td>
<td class="valign-bottom">Bottom-aligned content</td>
</tr>
</table>
✅ Fixed: applying a default to all cells in a table
If every cell in a table should share the same vertical alignment, target them with a single CSS rule:
<style>
.data-table td,
.data-table th {
vertical-align: top;
}
</style>
<table class="data-table">
<tr>
<th>Name</th>
<th>Description</th>
</tr>
<tr>
<td>Item A</td>
<td>A longer description that may<br>span multiple lines</td>
</tr>
</table>
This approach is the most maintainable — you set the alignment once and it applies consistently to every cell, without needing to modify individual <td> or <th> elements.
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.
¿Listo para validar tus sitios?
Comienza tu prueba gratuita hoy.