Accessibility Guides for WCAG 2.0 Experimental
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.
When a developer uses CSS to make a <p> element look like a heading — by increasing font size, adding bold weight, or applying other visual styling — sighted users may perceive it as a heading, but assistive technologies cannot. Screen readers identify headings by their semantic markup, not their visual appearance. A <p> element styled to look like a heading is still announced as a plain paragraph, which means the document’s structure is invisible to anyone who depends on programmatic heading navigation.
This issue primarily affects blind and deafblind users who rely on screen readers, as well as users with mobility impairments who navigate via keyboard shortcuts. Screen reader users frequently jump between headings to skim a page’s content — similar to how sighted users visually scan for larger, bolder text. When headings are not properly marked up, these users must listen through all content linearly, wasting significant time and effort.
This rule relates to WCAG 2.1 Success Criterion 1.3.1: Info and Relationships, which requires that information, structure, and relationships conveyed through presentation are programmatically determinable or available in text. When a paragraph is styled to look like a heading, the structural relationship it implies (a section label) is only conveyed visually and fails this criterion.
How to fix it
-
Identify styled paragraphs acting as headings. Look for
<p>elements with CSS that increasesfont-size, appliesfont-weight: bold, or otherwise makes them visually distinct in a way that suggests a heading. -
Replace them with semantic heading elements. Use
<h1>through<h6>based on the element’s position in the document hierarchy. -
Maintain a logical heading order. Start the main content with an
<h1>, use<h2>for major sections,<h3>for sub-sections within those, and so on. Avoid skipping levels (e.g., jumping from<h2>to<h4>). - Move visual styling to the heading element. Apply any desired CSS styles to the heading element instead of the paragraph.
As a best practice, the <h1> should appear at the beginning of the main content so screen reader users can jump directly to it using keyboard shortcuts. Sub-sections should use <h2>, with deeper nesting using <h3> through <h6> as needed. Headings should be brief, clear, and unique to maximize their usefulness as navigation landmarks.
Beyond accessibility, proper heading structure also benefits SEO, since search engines use headings to understand and rank page content.
Examples
Incorrect: styled paragraph used as a heading
In this example, a <p> element is visually styled to look like a heading but provides no semantic heading information to assistive technologies.
<style>
.fake-heading {
font-size: 24px;
font-weight: bold;
margin-top: 1em;
}
</style>
<p class="fake-heading">Our Services</p>
<p>We offer a wide range of consulting services.</p>
Correct: proper heading element with styling
Replace the styled <p> with an appropriate heading element. The same visual styles can be applied to the heading if needed.
<style>
h2 {
font-size: 24px;
font-weight: bold;
margin-top: 1em;
}
</style>
<h2>Our Services</h2>
<p>We offer a wide range of consulting services.</p>
Incorrect: multiple styled paragraphs mimicking a heading hierarchy
<p style="font-size: 32px; font-weight: bold;">Welcome to Our Site</p>
<p>Some introductory content here.</p>
<p style="font-size: 24px; font-weight: bold;">About Us</p>
<p>Learn more about our team.</p>
<p style="font-size: 18px; font-weight: bold;">Our Mission</p>
<p>We strive to make the web accessible.</p>
Correct: semantic heading hierarchy
<h1>Welcome to Our Site</h1>
<p>Some introductory content here.</p>
<h2>About Us</h2>
<p>Learn more about our team.</p>
<h3>Our Mission</h3>
<p>We strive to make the web accessible.</p>
What this rule checks
This rule examines <p> elements and flags any that have been styled to visually resemble headings through CSS properties such as increased font-size, font-weight: bold, or font-style: italic. If a paragraph’s styling makes it look like a heading, it should be converted to a proper heading element instead.
When a data table needs a visible title, developers sometimes create a row at the top of the table containing a single cell that spans all columns using the colspan attribute. While this may look like a caption visually, it is semantically incorrect. Screen readers treat it as a regular data or header cell, not as a table title. This means users who rely on assistive technologies — particularly people who are blind or deafblind — won’t hear the table’s purpose announced as a caption. Instead, they’ll encounter what appears to be just another cell of data, making it harder to understand the table’s context and contents.
HTML provides the <caption> element specifically for this purpose. When a screen reader encounters a <table> with a <caption>, it announces the caption text as the table’s name. This gives users immediate context about what the table contains before they start navigating rows and columns. Without a proper <caption>, users must guess the purpose of the table from its data alone.
Related WCAG Success Criteria
This rule relates to WCAG 2.1 Success Criterion 1.3.1: Info and Relationships (Level A), which requires that information, structure, and relationships conveyed through presentation are programmatically determinable. A fake caption created with colspan conveys a relationship visually (this text is the title of this table) but fails to communicate that relationship programmatically.
It also applies to Section 508 guidelines requiring that row and column headers be identified for data tables, and that markup be used to properly associate data cells and header cells.
How to Fix It
-
Remove the fake caption row — delete the
<tr>containing the cell with acolspanattribute that serves as a visual caption. -
Add a
<caption>element — place a<caption>element as the first child of the<table>element, containing the table’s title text. -
Style as needed — if you need the caption to look a certain way, apply CSS to the
<caption>element rather than reverting to acolspan-based approach.
Examples
Incorrect: Using colspan to fake a caption
This approach creates a visual title but is not recognized as a caption by assistive technologies.
<table>
<tr>
<td colspan="4"><strong>Quarterly Sales Report</strong></td>
</tr>
<tr>
<th scope="col">Region</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
</tr>
<tr>
<th scope="row">North</th>
<td>$12,000</td>
<td>$15,000</td>
<td>$13,500</td>
</tr>
</table>
Correct: Using the <caption> element
The <caption> element gives the table a programmatically associated name that screen readers announce automatically.
<table>
<caption>Quarterly Sales Report</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North</th>
<td>$12,000</td>
<td>$15,000</td>
<td>$13,500</td>
</tr>
</tbody>
</table>
Correct: Styled <caption> element
If you need the caption to match a specific visual design, style it with CSS rather than using table cells.
<style>
table.data caption {
font-size: 1.2em;
font-weight: bold;
text-align: left;
padding: 8px 0;
}
</style>
<table class="data">
<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>
What This Rule Checks
The table-fake-caption rule inspects data tables for <td> or <th> cells that use a colspan attribute spanning all columns, which suggests the cell is being used as a visual caption. If such a pattern is detected and no proper <caption> element is present, the rule flags the table as a violation.
Data tables communicate structured information where each cell’s meaning depends on its position relative to row and column headers. When you look at a table visually, your eyes naturally track back to the headers to understand what a particular value represents. Screen readers replicate this behavior by announcing the relevant headers as users navigate between cells — but only when the headers are properly marked up in the HTML.
Without proper header associations, a screen reader might announce a cell value like “28:04” with no context. The user would have no way to know what that number represents, who it belongs to, or what category it falls under. This is a critical accessibility barrier for people who are blind or deafblind.
This rule relates to WCAG 2.1 Success Criterion 1.3.1 (Info and Relationships), which requires that information and relationships conveyed through visual presentation be programmatically determinable. It is also required by Section 508 guidelines, which mandate that row and column headers be identified for data tables and that markup be used to associate data cells with header cells for tables with two or more logical levels of headers.
The axe rule td-has-header checks tables that are at least 3 cells wide and 3 cells tall. If any non-empty <td> in such a table lacks an associated header, the rule fails.
How to Fix It
There are two primary techniques for associating data cells with headers:
Using <th> with scope
This is the simplest and preferred approach for straightforward tables. Replace header cells’ <td> elements with <th> elements and add a scope attribute:
-
Use
scope="col"for column headers. -
Use
scope="row"for row headers.
Screen readers use the scope attribute to determine which data cells belong to which header. This approach works well for tables where each column and row has a single header.
Using id and headers
For complex tables — such as those with merged cells, multiple header levels, or irregular structures — use the id and headers method. Give each <th> a unique id, then add a headers attribute to each <td> listing the id values of all headers that apply to that cell, separated by spaces.
This method is more verbose but allows you to explicitly define exactly which headers relate to each data cell, regardless of table complexity. Where possible, consider breaking a complex table into multiple simpler tables, which benefits all users.
Examples
Failing: Table with no headers
This table has no <th> elements at all, so screen readers cannot associate any data cell with a header.
<table>
<tr>
<td>Name</td>
<td>1 mile</td>
<td>5 km</td>
<td>10 km</td>
</tr>
<tr>
<td>Mary</td>
<td>8:32</td>
<td>28:04</td>
<td>1:01:16</td>
</tr>
<tr>
<td>Betsy</td>
<td>7:43</td>
<td>26:47</td>
<td>55:38</td>
</tr>
</table>
Passing: Simple table with scope
Column headers use scope="col" and row headers use scope="row", giving every <td> a clear association.
<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>
<tr>
<th scope="row">Matt</th>
<td>7:55</td>
<td>27:29</td>
<td>57:04</td>
</tr>
</tbody>
</table>
When a screen reader user navigates to the cell containing “26:47”, the screen reader announces something like “Betsy, 5 km, 26:47” — providing full context.
Passing: Complex table with id and headers
When a table has multiple levels of headers or merged cells, the id and headers method gives you explicit control over associations.
<table>
<caption>Race Results by Category</caption>
<thead>
<tr>
<td></td>
<th id="road" colspan="2">Road Races</th>
<th id="track" colspan="2">Track Events</th>
</tr>
<tr>
<td></td>
<th id="r5k">5 km</th>
<th id="r10k">10 km</th>
<th id="t800">800 m</th>
<th id="t1500">1500 m</th>
</tr>
</thead>
<tbody>
<tr>
<th id="mary">Mary</th>
<td headers="mary road r5k">28:04</td>
<td headers="mary road r10k">1:01:16</td>
<td headers="mary track t800">2:34</td>
<td headers="mary track t1500">5:51</td>
</tr>
<tr>
<th id="betsy">Betsy</th>
<td headers="betsy road r5k">26:47</td>
<td headers="betsy road r10k">55:38</td>
<td headers="betsy track t800">2:17</td>
<td headers="betsy track t1500">5:09</td>
</tr>
</tbody>
</table>
In this example, when a screen reader lands on “26:47”, it can announce “Betsy, Road Races, 5 km, 26:47” because the headers attribute explicitly lists all three relevant header id values.
Key points to remember
-
Always use
<th>for header cells, not styled<td>elements. -
Add a
<caption>to give your table a descriptive name. -
Use
scopefor simple tables andid+headersfor complex ones. -
Empty
<td>cells are excluded from this rule — only non-empty data cells need header associations. - Visual styling (borders, fonts, backgrounds) should be handled with CSS and has no impact on accessibility markup.
Ready to validate your sites?
Start your free trial today.