Accessibility Guides for WCAG 2.1 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 web page uses CSS media queries like @media (orientation: portrait) or @media (orientation: landscape) to force content into a single orientation, it prevents the page from responding to the user’s actual device position. This is checked by the axe rule css-orientation-lock.
Why this matters
Many users physically cannot rotate their devices. People with mobility impairments may have their phone or tablet mounted to a wheelchair, bed, or desk in a fixed orientation. Users with low vision may prefer landscape mode to enlarge text, while others may find portrait easier to read. Locking orientation removes their ability to choose what works best for them.
Beyond motor and vision disabilities, orientation locking also affects users with cognitive disabilities and dyslexia who may rely on a particular layout for readability. Sighted keyboard users who use external displays or stands may also be impacted.
This rule relates to WCAG 2.1 Success Criterion 1.3.4: Orientation (Level AA), which requires that content not restrict its view and operation to a single display orientation unless a specific orientation is essential. Essential use cases are rare — examples include a piano keyboard app, a bank check deposit interface, or a presentation slide display where the orientation is integral to the functionality.
How to fix it
-
Remove orientation-locking CSS. Look for
@mediaqueries that use theorientationfeature combined with styles that effectively hide or completely rearrange content for only one orientation (e.g., settingdisplay: noneorwidth: 0on the body or main content). -
Use responsive design instead. Rather than checking orientation, use
min-widthormax-widthmedia queries to adapt your layout to available space. This naturally accommodates both orientations. - Test in both orientations. Rotate your device or use browser developer tools to simulate both portrait and landscape modes. Verify that all content remains visible and functional.
- Only lock orientation when essential. If your application genuinely requires a specific orientation for core functionality (not just aesthetic preference), document the rationale. This is the only valid exception.
Examples
Incorrect: Locking content to portrait only
This CSS hides the main content area when the device is in landscape orientation, effectively forcing users to use portrait mode:
<style>
@media (orientation: landscape) {
.content {
display: none;
}
.rotate-message {
display: block;
}
}
@media (orientation: portrait) {
.rotate-message {
display: none;
}
}
</style>
<div class="content">
<h1>Welcome to our site</h1>
<p>This content is only visible in portrait mode.</p>
</div>
<div class="rotate-message">
<p>Please rotate your device to portrait mode.</p>
</div>
Incorrect: Using transform to force portrait layout in landscape
<style>
@media (orientation: landscape) {
body {
transform: rotate(-90deg);
transform-origin: top left;
width: 100vh;
height: 100vw;
overflow: hidden;
position: absolute;
}
}
</style>
This forcibly rotates the entire page, fighting against the user’s chosen orientation and creating a confusing, inaccessible experience.
Correct: Responsive layout that works in both orientations
<style>
.content {
padding: 1rem;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 600px) {
.grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
<div class="content">
<h1>Welcome to our site</h1>
<div class="grid">
<div>
<p>Column one content.</p>
</div>
<div>
<p>Column two content.</p>
</div>
</div>
</div>
This approach uses min-width instead of orientation, adapting the layout based on available space. The content remains fully accessible and readable whether the device is held in portrait or landscape.
Correct: Using orientation queries for minor style adjustments (not locking)
<style>
.hero-image {
width: 100%;
height: 200px;
object-fit: cover;
}
@media (orientation: landscape) {
.hero-image {
height: 300px;
}
}
</style>
<img class="hero-image" src="hero.jpg" alt="A scenic mountain landscape">
Using orientation media queries is acceptable when you’re making minor visual adjustments without hiding or restricting access to content. The key is that all content and functionality remain available in both orientations.
When an interactive element displays visible text — like a button saying “Submit” — users naturally expect that text to be the element’s name. However, developers sometimes use aria-label or aria-labelledby to set an accessible name that differs from the visible text. This creates a disconnect: what sighted users see and what assistive technologies announce become two different things.
This is a serious accessibility problem that primarily affects speech input users. These users interact with web pages by speaking the names of controls they see on screen. If someone sees a link labeled “Next Page” and says “click Next Page,” but the accessible name is actually “OK,” the speech command fails. The user has no way to know the correct programmatic name, leading to frustration and an inability to use the interface.
This issue also affects screen reader users and users with cognitive disabilities. When a screen reader announces a name that doesn’t match the visible label, it creates confusion — the user may not be sure they’re focused on the right element. For users with cognitive disabilities who rely on consistent, predictable interfaces, this mismatch adds unnecessary complexity.
This rule checks compliance with WCAG 2.1 Success Criterion 2.5.3: Label in Name (Level A). This criterion requires that when a user interface component has a visible text label, the accessible name must contain that visible text. The intent is to ensure that the words users see on screen can be used to interact with the component, regardless of input method.
The rule applies to elements that meet all three conditions:
-
The element has a widget role that supports name from content — specifically:
button,checkbox,gridcell,link,menuitem,menuitemcheckbox,menuitemradio,option,radio,searchbox,switch,tab, ortreeitem. - The element has visible text content.
-
The element has an
aria-labeloraria-labelledbyattribute that overrides the default accessible name.
When evaluating the match, leading and trailing whitespace is ignored, and the comparison is case-insensitive. The complete visible text must either match the accessible name exactly or be fully contained within it.
How to Fix
To resolve this issue:
-
Make the accessible name include the full visible text. If the element’s visible text is “Next Page,” the
aria-labelmust contain “Next Page” — not just part of it, and not something completely different. - Start the accessible name with the visible text. While not strictly required, it’s best practice. This helps speech input users who may only speak the beginning of a label.
-
Consider removing the
aria-labelentirely. If the visible text alone is a sufficient accessible name, you may not need an override at all. The simplest fix is often to let the element derive its name from its content naturally.
Examples
Failing: Accessible name doesn’t match visible text
The visible text says “Next” but the accessible name is “OK”:
<div role="link" aria-label="OK">Next</div>
Failing: Accessible name only contains part of the visible text
The visible text is “The full label” but the accessible name is “the full,” which doesn’t include the complete visible text:
<button aria-label="the full">The full label</button>
Passing: Accessible name matches visible text
The aria-label matches the visible text (trailing whitespace and case differences are ignored):
<div role="link" aria-label="Next Page">next page</div>
Passing: Accessible name contains the full visible text
The visible text “Next Page” is fully contained within the accessible name:
<button aria-label="Next Page in the list">Next Page</button>
Passing: No aria-label override needed
When the visible text is already a good accessible name, simply omit the aria-label:
<button>Next Page</button>
Passing: aria-labelledby references text that includes the visible content
<span id="btn-label">Search the full catalog</span>
<button aria-labelledby="btn-label">Search</button>
Here, the visible text “Search” is contained within the accessible name “Search the full catalog,” so the rule passes. The accessible name begins with the visible label, which is ideal for speech input users.
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.