Accessibility Guides for deafblind
Learn how to identify and fix common accessibility issues flagged by Axe Core — so your pages are inclusive and usable for everyone. Also check our HTML Validation Guides.
The headers attribute is one of the primary ways to programmatically associate data cells with their corresponding header cells in HTML tables. It works by referencing the id values of th elements. When these references point to elements outside the table — or to id values that don’t exist at all — the association breaks, and assistive technology can no longer convey the relationship between data and headers.
This primarily affects users who are blind or deafblind and rely on screen readers. When navigating a data table, screen readers announce the relevant column and row headers as users move from cell to cell. This “table navigation mode” is essential for understanding the data in context. If the headers attribute references are broken, users may hear no header announcements or incorrect ones, making it impossible to interpret the data.
This rule relates to WCAG 2.0, 2.1, and 2.2 Success Criterion 1.3.1: Info and Relationships (Level A), which requires that information, structure, and relationships conveyed visually are also available programmatically. It also applies to Section 508 requirements that row and column headers be identified for data tables and that markup be used to associate data cells with header cells in tables with two or more logical levels of headers.
How to Fix the Problem
There are three common approaches to associating headers with data cells:
Use the scope attribute (simplest approach)
For straightforward tables, adding scope="col" to column headers and scope="row" to row headers is the easiest and most reliable method. No id or headers attributes are needed.
Use id and headers attributes (for complex tables)
For tables with multiple levels of headers or irregular structures, you can assign an id to each th element and then list the relevant id values in each td‘s headers attribute. The critical rule is: every id referenced in a headers attribute must belong to a th in the same <table> element.
Use scope="colgroup" and scope="rowgroup"
For tables with headers that span multiple columns or rows, the colgroup and rowgroup values of scope can indicate which group of columns or rows a header applies to.
Examples
Incorrect: headers attribute references an id outside the table
In this example, the headers attribute on data cells references id="name", which exists on an element outside the table. This will trigger the rule violation.
<h2 id="name">Name</h2>
<table>
<tr>
<th id="time">Time</th>
</tr>
<tr>
<td headers="name time">Mary - 8:32</td>
</tr>
</table>
Incorrect: headers attribute references a non-existent id
Here, headers="runner" refers to an id that doesn’t exist anywhere in the table.
<table>
<tr>
<th id="name">Name</th>
<th id="time">1 Mile</th>
</tr>
<tr>
<td headers="runner">Mary</td>
<td headers="runner time">8:32</td>
</tr>
</table>
Correct: Using scope for a simple table
For simple tables, scope is the preferred method and avoids the pitfalls of headers entirely.
<table>
<caption>Greensprings Running Club Personal Bests</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">1 Mile</th>
<th scope="col">5 km</th>
<th scope="col">10 km</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Mary</th>
<td>8:32</td>
<td>28:04</td>
<td>1:01:16</td>
</tr>
<tr>
<th scope="row">Betsy</th>
<td>7:43</td>
<td>26:47</td>
<td>55:38</td>
</tr>
</tbody>
</table>
Correct: Using id and headers for a complex table
When a table has multiple levels of headers, the headers attribute allows you to explicitly associate each data cell with the correct set of headers. All referenced id values are on th elements within the same table.
<table>
<caption>Items Sold August 2016</caption>
<thead>
<tr>
<td colspan="2"></td>
<th id="clothes" scope="colgroup" colspan="3">Clothes</th>
<th id="accessories" scope="colgroup" colspan="2">Accessories</th>
</tr>
<tr>
<td colspan="2"></td>
<th id="trousers" scope="col">Trousers</th>
<th id="skirts" scope="col">Skirts</th>
<th id="dresses" scope="col">Dresses</th>
<th id="bracelets" scope="col">Bracelets</th>
<th id="rings" scope="col">Rings</th>
</tr>
</thead>
<tbody>
<tr>
<th id="belgium" scope="rowgroup" rowspan="3">Belgium</th>
<th id="antwerp" scope="row">Antwerp</th>
<td headers="belgium antwerp clothes trousers">56</td>
<td headers="belgium antwerp clothes skirts">22</td>
<td headers="belgium antwerp clothes dresses">43</td>
<td headers="belgium antwerp accessories bracelets">72</td>
<td headers="belgium antwerp accessories rings">23</td>
</tr>
<tr>
<th id="gent" scope="row">Gent</th>
<td headers="belgium gent clothes trousers">46</td>
<td headers="belgium gent clothes skirts">18</td>
<td headers="belgium gent clothes dresses">50</td>
<td headers="belgium gent accessories bracelets">61</td>
<td headers="belgium gent accessories rings">15</td>
</tr>
<tr>
<th id="brussels" scope="row">Brussels</th>
<td headers="belgium brussels clothes trousers">51</td>
<td headers="belgium brussels clothes skirts">27</td>
<td headers="belgium brussels clothes dresses">38</td>
<td headers="belgium brussels accessories bracelets">69</td>
<td headers="belgium brussels accessories rings">28</td>
</tr>
</tbody>
</table>
In this complex example, each data cell’s headers attribute lists the id values of every relevant header — the country group, the city, the product category, and the specific product. Every referenced id belongs to a th within the same <table>, so the associations are valid and screen readers can announce the full context for each cell.
Tips for avoiding this issue
-
Double-check
idvalues. A common cause of this violation is a typo in either theidon thethor in theheadersattribute value on thetd. -
Don’t reuse
idvalues across tables. If you copy markup from one table to another, ensureidvalues are unique within the page and thatheadersattributes reference the correct table’sthelements. -
Prefer
scopewhen possible. For simple tables with a single row of column headers and/or a single column of row headers,scopeis simpler and less error-prone thanid/headers. -
Use
idandheadersfor genuinely complex tables — those with multiple header levels, merged cells, or irregular structures wherescopealone cannot convey all relationships.
Screen readers rely on the programmatic relationships between header cells and data cells to help users navigate and understand tabular data. As a user moves through a table, the screen reader announces the relevant column or row header for each data cell, giving context to what would otherwise be just a raw value. When a <th> element doesn’t properly refer to any data cells, the screen reader either announces incorrect headers, skips the header entirely, or produces confusing output.
This primarily affects blind and deafblind users who depend on screen readers to interpret table structure. Without correct header associations, these users cannot understand what each piece of data represents or how it relates to other data in the table.
Related Standards
This rule maps to WCAG 2.0, 2.1, and 2.2 Success Criterion 1.3.1: Info and Relationships (Level A), which requires that information, structure, and relationships conveyed through presentation are programmatically determinable. A table header that doesn’t actually reference any data cells fails to convey the intended structural relationship.
It also relates to Section 508 requirements that row and column headers be identified for data tables, and that markup be used to associate data cells and header cells. The Trusted Tester guidelines similarly require that all data cells are programmatically associated with relevant headers.
How to Fix It
-
Use the correct
scopevalue on<th>elements. If the header applies to a column, usescope="col". If it applies to a row, usescope="row". A common mistake is usingscope="row"on column headers or vice versa, which means the header doesn’t actually point to any data cells in the intended direction. -
Ensure every
<th>has data cells to reference. A<th>that sits in a position where it has no associated<td>elements (e.g., an empty row or column) should either be restructured or converted to a<td>. -
For simple tables, use
scopeon<th>elements. This is the simplest and most reliable approach when there is a single row of column headers and/or a single column of row headers. -
For complex tables with multiple levels of headers, use
idandheadersattributes. Assign a uniqueidto each<th>, then reference the appropriateidvalues in theheadersattribute of each<td>. Note that<th>elements themselves should not use theheadersattribute to reference other<th>elements — keepheaderson<td>elements. -
If using
headersattributes, ensure the referencedidvalues point to elements with text content that is available to screen readers.
Examples
Incorrect: scope="row" Used on Column Headers
In this example, the <th> elements are column headers, but they are incorrectly scoped to row. This means the headers don’t reference the data cells below them, so screen readers cannot associate “Last Name,” “First Name,” or “City” with their respective columns.
<table>
<caption>Teddy bear collectors</caption>
<tr>
<th scope="row">Last Name</th>
<th scope="row">First Name</th>
<th scope="row">City</th>
</tr>
<tr>
<td>Phoenix</td>
<td>Imari</td>
<td>Henry</td>
</tr>
<tr>
<td>Zeki</td>
<td>Rome</td>
<td>Min</td>
</tr>
</table>
Correct: scope="col" Used on Column Headers
The fix is straightforward — change scope="row" to scope="col" so the headers correctly reference the data cells beneath them.
<table>
<caption>Teddy bear collectors</caption>
<tr>
<th scope="col">Last Name</th>
<th scope="col">First Name</th>
<th scope="col">City</th>
</tr>
<tr>
<td>Phoenix</td>
<td>Imari</td>
<td>Henry</td>
</tr>
<tr>
<td>Zeki</td>
<td>Rome</td>
<td>Min</td>
</tr>
</table>
Correct: Using Both Column and Row Headers
When a table has headers in both the first row and the first column, use scope="col" for the column headers and scope="row" for the row headers.
<table>
<caption>Quarterly sales (in thousands)</caption>
<tr>
<td></td>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
</tr>
<tr>
<th scope="row">Widgets</th>
<td>50</td>
<td>80</td>
<td>120</td>
</tr>
<tr>
<th scope="row">Gadgets</th>
<td>30</td>
<td>45</td>
<td>60</td>
</tr>
</table>
Correct: Complex Table Using id and headers
For tables with multiple levels of headers, use the id and headers approach to create explicit associations.
<table>
<caption>Student exam results</caption>
<tr>
<td></td>
<th id="math" scope="col">Math</th>
<th id="science" scope="col">Science</th>
</tr>
<tr>
<th id="maria" scope="row">Maria</th>
<td headers="maria math">95</td>
<td headers="maria science">88</td>
</tr>
<tr>
<th id="james" scope="row">James</th>
<td headers="james math">72</td>
<td headers="james science">91</td>
</tr>
</table>
In this example, each <td> explicitly references both its row header and column header, so a screen reader can announce something like “Maria, Math, 95” as the user navigates the table.
Screen readers rely on language information to select the correct pronunciation engine for content. Each language has its own sound library with unique pronunciation rules, intonation, and phonetic patterns. When a screen reader encounters a lang attribute with a valid value, it seamlessly switches to the appropriate library. But when the value is invalid — for example, lang="egnlish" instead of lang="en" — the screen reader cannot identify the language and falls back to its default, reading the foreign-language text with completely wrong pronunciation.
This primarily affects blind users and deafblind users who depend on screen readers, as well as users with cognitive disabilities who may rely on text-to-speech tools. Imagine hearing Spanish text pronounced with English phonetic rules — it would be nearly incomprehensible.
Note that this rule specifically checks lang attributes on elements within the page (not the <html> element itself, which is covered by a separate rule). It applies whenever you use the lang attribute to indicate that a section of content is in a different language than the rest of the page.
Related WCAG Success Criteria
This rule maps to WCAG 2.0 / 2.1 / 2.2 Success Criterion 3.1.2: Language of Parts (Level AA). This criterion requires that the human language of each passage or phrase in the content can be programmatically determined, except for proper names, technical terms, and words of indeterminate language. Using a valid language code is essential to meeting this requirement — an invalid code fails to programmatically convey the language.
This rule is also referenced by the Trusted Tester methodology, EN 301 549, and RGAA.
How to Fix It
-
Use valid language codes. Language values must conform to the BCP 47 standard. In practice, this means using two- or three-letter codes from the IANA Language Subtag Registry. Common examples include
en(English),fr(French),es(Spanish),de(German),zh(Chinese),ar(Arabic), andja(Japanese). -
Check for typos. Invalid values are often caused by misspellings (e.g.,
lang="enlish") or using full language names instead of codes (e.g.,lang="French"instead oflang="fr"). -
Use dialect subtags when appropriate. You can specify regional variants such as
en-US(American English),en-GB(British English), orfr-CA(Canadian French). The primary subtag alone is also valid. -
Set the
dirattribute for RTL languages. If you’re marking content in a right-to-left language like Arabic or Hebrew, pair thelangattribute withdir="rtl"to ensure correct text rendering.
Examples
Incorrect: Invalid language code
<p>Welcome to our site. <span lang="spn">Bienvenidos a nuestro sitio.</span></p>
The value spn is not a valid language subtag. Screen readers cannot identify this as Spanish.
Incorrect: Full language name instead of code
<p>Here is a quote: <q lang="Japanese">素晴らしい</q></p>
The value Japanese is not a valid BCP 47 language code.
Correct: Valid two-letter language code
<p>Welcome to our site. <span lang="es">Bienvenidos a nuestro sitio.</span></p>
The value es is the valid language subtag for Spanish.
Correct: Valid language code with regional subtag
<p>The Canadian term is <span lang="fr-CA">dépanneur</span>.</p>
The value fr-CA correctly identifies Canadian French.
Correct: RTL language with direction attribute
<p>The Arabic word for peace is <span lang="ar" dir="rtl">سلام</span>.</p>
The value ar is the valid subtag for Arabic, and dir="rtl" ensures proper text directionality.
Correct: Multiple language switches on one page
<p>
In English we say "goodbye," in German it's
<span lang="de">Auf Wiedersehen</span>, and in Japanese it's
<span lang="ja">さようなら</span>.
</p>
Each inline element uses a valid language code, allowing the screen reader to switch pronunciation engines as needed.
Ready to validate your sites?
Start your free trial today.