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.
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
headersvalue or thethelement'sid. - Missing
id— Thethelement exists but doesn't have anidattribute assigned. - Removed or renamed headers — The
thwas deleted or itsidwas changed during refactoring, but thetdstill references the old value. - Cross-table references — The
thwith the referencedidexists 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
tdelement flagged by the validator and note the ID it references. - Search the same
<table>for athelement with a matchingid. - If the
thexists but has noidor a differentid, add or correct theidattribute so it matches. - If the
thwas removed, either restore it or remove theheadersattribute from thetd. - Double-check for case sensitivity — HTML
idvalues are case-sensitive, soheaders="Name"does not matchid="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 name attribute is not a valid attribute for the <table> element in HTML.
The name attribute was never part of any HTML specification for tables. Some older browsers tolerated it as a way to create anchor targets, but validators reject it because it does not belong on <table>. If the goal is to link directly to a table, use the id attribute instead. The id attribute is a global attribute, valid on any HTML element, and works as a fragment identifier in URLs (e.g., page.html#prices).
If the name attribute was being used for JavaScript access, switch to id and use document.getElementById(), or use a data-* attribute for storing custom metadata.
Invalid example
<table name="prices">
<tr>
<td>Item</td>
<td>Price</td>
</tr>
</table>
Valid example
Using id as a fragment target:
<table id="prices">
<tr>
<td>Item</td>
<td>Price</td>
</tr>
</table>
If custom metadata is needed rather than a link target, a data-* attribute works:
<table data-name="prices">
<tr>
<td>Item</td>
<td>Price</td>
</tr>
</table>
A <td> inside a <table> with no role, or with a role of table, grid or treegrid, gets its ARIA role from the table and must not have a role attribute of its own.
Browsers map table cells automatically. In a plain table, or one with role="table", each <td> is exposed as a cell. In a table with role="grid" or role="treegrid", each <td> is exposed as a gridcell. The ARIA in HTML specification doesn't let authors set any role on these cells, so the validator rejects redundant values like role="cell" and role="gridcell" as well as roles that turn the cell into something else, such as role="button" or role="presentation".
A <td> exposed as a button is no longer a cell, so screen readers can't announce its row and column headers or reach it with table navigation commands.
To make a cell interactive, put a real control such as a <button> or an <a> inside the <td>. For layout tables, which are common in HTML email, set role="presentation" on the <table> instead of on each cell. Once the table has a role other than table, grid or treegrid, its cells lose their table semantics and the rule no longer applies.
Invalid examples
Data table
<table>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
<tr>
<td role="cell">Alice</td>
<td role="button" tabindex="0">Edit</td>
</tr>
</table>
Layout table
<table>
<tr>
<td role="presentation">Logo</td>
<td role="presentation">Newsletter title</td>
</tr>
</table>
Valid examples
Data table
<table>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
<tr>
<td>Alice</td>
<td><button type="button">Edit</button></td>
</tr>
</table>
Layout table
<table role="presentation">
<tr>
<td>Logo</td>
<td>Newsletter title</td>
</tr>
</table>
A <tr> element already has an implicit ARIA role of row, so adding role="row" is redundant when the parent <table> uses its default semantics or has a role of table, grid, or treegrid.
HTML tables come with built-in accessibility semantics. The <table> element implicitly has role="table", and <tr> implicitly has role="row". Browsers and assistive technologies already understand this structure, so explicitly adding these roles is unnecessary and flagged by the W3C validator.
The only time you'd need to add a role to a <tr> is when the table's native semantics have been overridden — for example, if the <table> has been repurposed with a non-table role like role="presentation" or role="none". In that case, you'd need explicit ARIA roles to restore row semantics.
Incorrect Example
<table>
<tr role="row">
<th>Name</th>
<th>Email</th>
</tr>
<tr role="row">
<td>Alice</td>
<td>alice@example.com</td>
</tr>
</table>
Fixed Example
Simply remove the redundant role="row" from the <tr> elements:
<table>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
<tr>
<td>Alice</td>
<td>alice@example.com</td>
</tr>
</table>
The same fix applies if your <table> explicitly has role="table", role="grid", or role="treegrid" — the <tr> elements still don't need an explicit role="row" because the browser infers it automatically.
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>
The summary attribute was used in HTML 4 to provide a text description of a table's structure and purpose, primarily for screen reader users. In HTML5, this attribute was deprecated because it was invisible to sighted users, creating an unequal experience. It was also frequently misused — authors often duplicated the table's caption or provided unhelpful descriptions, diminishing its accessibility value.
The HTML Living Standard offers several better alternatives, each suited to different situations:
- Use a
<caption>element — Best for a concise title or description that benefits all users, not just screen reader users. The<caption>must be the first child of the<table>element. - Use a
<figure>with<figcaption>— Ideal when you want to provide a longer description or contextual information alongside the table. This approach also semantically groups the table with its description. - Simplify the table — If your table is straightforward with clear headers, it may not need any additional description at all. Well-structured
<th>elements with appropriatescopeattributes often provide enough context.
From an accessibility standpoint, the <caption> and <figcaption> approaches are superior because they are visible to all users and part of the document flow. Screen readers announce <caption> content when a user navigates to a table, providing the same benefit the summary attribute once offered — but now everyone can see it.
Examples
❌ Obsolete: Using the summary attribute
This triggers the validation warning because summary is no longer a valid attribute on <table>.
<table summary="This table shows monthly sales figures for 2024.">
<tr>
<th>Month</th>
<th>Sales</th>
</tr>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</table>
✅ Fix 1: Using a <caption> element
Replace the summary attribute with a <caption> as the first child of the <table>. This is the most common and straightforward fix.
<table>
<caption>Monthly sales figures for 2024</caption>
<tr>
<th>Month</th>
<th>Sales</th>
</tr>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</table>
✅ Fix 2: Using <figure> and <figcaption>
This approach is useful when you want to provide a longer description or when the table is referenced as a figure within surrounding content.
<figure>
<figcaption>
Monthly sales figures for 2024, showing a steady increase in revenue
during the first quarter.
</figcaption>
<table>
<tr>
<th>Month</th>
<th>Sales</th>
</tr>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</table>
</figure>
✅ Fix 3: Simplify and rely on clear headers
For simple tables where the data is self-explanatory, well-labeled headers with scope attributes may be sufficient. No extra description is needed.
<table>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Sales</th>
</tr>
</thead>
<tbody>
<tr>
<td>January</td>
<td>$1,000</td>
</tr>
<tr>
<td>February</td>
<td>$1,200</td>
</tr>
</tbody>
</table>
You can also combine approaches — use a <caption> for a brief title and wrap the table in a <figure> with a <figcaption> for additional context. The key takeaway is to remove the summary attribute and use visible, semantic HTML elements to describe your table instead.
The role="table" attribute on a <table> element is redundant because the <table> element already has an implicit ARIA role of table.
Every HTML element carries a default ARIA role defined by the HTML specification. The <table> element's built-in role is table, so adding role="table" explicitly tells assistive technologies something they already know. The W3C validator flags this as unnecessary markup.
This applies to many other elements too. A <nav> element has an implicit role of navigation, a <button> has a role of button, and so on. Adding these explicit roles creates noise in the code without any accessibility benefit.
Incorrect example
<table role="table">
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Alice</td>
<td>30</td>
</tr>
</table>
Fixed example
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Alice</td>
<td>30</td>
</tr>
</table>
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 towidth: 200pxin CSS. - Consider responsive design — this is a good opportunity to switch from fixed pixel widths to percentages,
emunits, or other flexible values. - Use
table-layout: fixedon the<table>element if you need columns to respect exact widths rather than auto-sizing based on content.
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