Guias de acessibilidade para RGAA
Aprenda como identificar e corrigir problemas de acessibilidade comuns sinalizados pelo Axe Core — para que suas páginas sejam inclusivas e utilizáveis por todos. Consulte também o nosso Guias de validação HTML.
The <object> element embeds external content into a web page — this could be a video, an audio player, an interactive applet, a PDF, or even another HTML page. Unlike images, which have a straightforward alt attribute, <object> elements rely on other mechanisms to provide text alternatives. When no alternative text is present, screen readers encounter the embedded object but have nothing meaningful to announce, leaving blind and deafblind users unable to understand what the content is or what purpose it serves.
Why this matters
Screen readers cannot interpret non-text content on their own. They depend entirely on text alternatives provided in the markup. When an <object> element lacks alternative text, users of assistive technology are left with no information about the embedded content. This is a serious barrier that can cause users to miss critical functionality or information on a page.
This rule relates to WCAG 2.0/2.1/2.2 Success Criterion 1.1.1: Non-text Content (Level A), which requires that all non-text content presented to the user has a text alternative that serves an equivalent purpose. It is also required by Section 508 and EN 301 549.
How to fix it
Add alternative text to every <object> element using one of these approaches:
-
titleattribute — Add atitleattribute directly on the<object>element with a concise description. -
aria-labelattribute — Provide an accessible name usingaria-label. -
aria-labelledbyattribute — Reference theidof another element that contains the descriptive text. -
role="presentation"orrole="none"— If the embedded object is purely decorative and conveys no information, you can remove it from the accessibility tree entirely.
Writing effective alternative text
When crafting alternative text, consider these questions:
- Why is this embedded content on the page?
- What information does it present to sighted users?
- What purpose or function does it serve?
- If you removed the object, what words would you use to convey the same information?
Keep the text concise and meaningful. Avoid generic terms like “object,” “video,” or file names — instead, describe the content’s actual purpose. For example, “Quarterly revenue chart for 2024” is far more useful than “chart.swf.”
Examples
Incorrect: no alternative text
The <object> element has no accessible name, so screen readers cannot describe it.
<object data="quarterly-report.pdf"></object>
Incorrect: inner content used as fallback text
Text placed inside the <object> element is intended as fallback content for browsers that cannot render the object — it is not reliably exposed as an accessible name by screen readers.
<object data="quarterly-report.pdf">
This object shows the quarterly report.
</object>
Incorrect: only whitespace inside the element
<object data="quarterly-report.pdf">
<div> </div>
</object>
Correct: using title
<object data="quarterly-report.pdf" title="2024 Q3 quarterly revenue report"></object>
Correct: using aria-label
<object data="quarterly-report.pdf" aria-label="2024 Q3 quarterly revenue report"></object>
Correct: using aria-labelledby
<h2 id="report-heading">2024 Q3 Quarterly Revenue Report</h2>
<object data="quarterly-report.pdf" aria-labelledby="report-heading"></object>
Correct: decorative object with role="none"
If the embedded object is purely decorative and adds no informational value, remove it from the accessibility tree:
<object data="decorative-animation.swf" role="none"></object>
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.
Landmark regions give a web page its structural backbone. They allow screen reader users to jump directly to major sections — like the navigation, main content, or footer — without having to tab or arrow through every element on the page. This is similar to how sighted users visually scan a page layout to find what they need.
When content exists outside of any landmark, screen readers may skip over it entirely during landmark navigation, or users may encounter it without any context about what section of the page it belongs to. This primarily affects users who are blind or deafblind, as well as users with mobility impairments who rely on assistive technologies to navigate efficiently.
This rule is flagged as a Deque best practice and is also referenced in the RGAA (the French government accessibility standard). While it doesn’t map directly to a specific WCAG success criterion, it strongly supports WCAG 1.3.1 (Info and Relationships) and WCAG 2.4.1 (Bypass Blocks) by ensuring that page structure is programmatically conveyed to assistive technologies.
How Landmarks Work
HTML5 introduced semantic elements that automatically create landmark regions:
| HTML5 Element | Equivalent ARIA Role | Purpose |
|---|---|---|
<header> |
banner |
Site-wide header (when not nested inside <article> or <section>) |
<nav> |
navigation |
Navigation links |
<main> |
main |
Primary page content |
<footer> |
contentinfo |
Site-wide footer (when not nested inside <article> or <section>) |
<aside> |
complementary |
Supporting content |
<section> (with accessible name) |
region |
A distinct section of the page |
<form> (with accessible name) |
form |
A form region |
Using native HTML5 elements is preferred over ARIA roles. The general principle is: if a native HTML element provides the landmark semantics you need, use it instead of adding role attributes to generic <div> elements.
How to Fix the Problem
-
Audit your page for any content that sits directly inside
<body>without being wrapped in a landmark element. -
Wrap orphaned content in the appropriate landmark. Most body content belongs inside
<main>. Headers and footers belong in<header>and<footer>. Navigation belongs in<nav>. - Skip links are the exception — a skip navigation link placed before the first landmark is acceptable and does not need to be wrapped in a landmark.
-
Use
<main>as a catch-all for any content that doesn’t belong in the header, footer, or navigation. Every page should have exactly one<main>element.
Examples
Incorrect: Content Outside Landmarks
In this example, the introductory paragraph and the promotional banner sit outside any landmark region. A screen reader user navigating by landmarks would miss them.
<body>
<header>
<h1>My Website</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<p>Welcome to our site! Check out our latest offerings.</p>
<main>
<h2>Featured Products</h2>
<p>Browse our catalog below.</p>
</main>
<div class="promo-banner">
<p>Free shipping on orders over $50!</p>
</div>
<footer>
<p>© 2024 My Website</p>
</footer>
</body>
The <p> element after <header> and the <div class="promo-banner"> are not inside any landmark.
Correct: All Content Inside Landmarks
Move the orphaned content into appropriate landmarks:
<body>
<header>
<h1>My Website</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<p>Welcome to our site! Check out our latest offerings.</p>
<h2>Featured Products</h2>
<p>Browse our catalog below.</p>
<aside>
<p>Free shipping on orders over $50!</p>
</aside>
</main>
<footer>
<p>© 2024 My Website</p>
</footer>
</body>
Correct: Basic Page Structure with HTML5 Landmarks
<body>
<header>
<h1>Site Name</h1>
</header>
<nav>
<a href="/">Home</a>
<a href="/contact">Contact</a>
</nav>
<main>
<h2>Page Title</h2>
<p>All primary content goes here.</p>
</main>
<footer>
<p>© 2024 Site Name</p>
</footer>
</body>
Correct: Using ARIA Roles (When HTML5 Elements Are Not an Option)
If you cannot use HTML5 semantic elements, apply ARIA landmark roles to generic elements:
<body>
<div role="banner">
<h1>Site Name</h1>
</div>
<div role="navigation">
<a href="/">Home</a>
<a href="/contact">Contact</a>
</div>
<div role="main">
<h2>Page Title</h2>
<p>All primary content goes here.</p>
</div>
<div role="contentinfo">
<p>© 2024 Site Name</p>
</div>
</body>
This approach is valid but less preferred. Native HTML5 elements are clearer, require less code, and are better supported across browsers and assistive technologies.
Correct: Skip Link Before Landmarks
A skip navigation link placed before all landmarks is the one accepted exception to this rule:
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>
<h1>Site Name</h1>
</header>
<nav>
<a href="/">Home</a>
</nav>
<main id="main-content">
<h2>Page Content</h2>
<p>This is the main content area.</p>
</main>
<footer>
<p>© 2024 Site Name</p>
</footer>
</body>
The skip link exists outside any landmark, but this is intentional and will not trigger the rule.
When you apply role="img" to an element, you’re telling assistive technologies to treat that element as a single image. This is commonly used to group decorative SVG elements, icon fonts, CSS background images, or multiple visual elements into one cohesive image. However, applying the role alone isn’t enough — assistive technologies also need a text alternative that describes the image’s content or purpose.
Without an accessible name, screen readers will either skip the element entirely or announce it as an unlabeled image, leaving users who rely on assistive technology without critical information. This affects screen reader users (who hear content read aloud), braille display users (who read content through tactile output), and to some extent users with low vision who may use screen readers alongside magnification.
This rule relates to WCAG 2.1 Success Criterion 1.1.1 (Non-text Content), which requires that all non-text content presented to the user has a text alternative that serves the equivalent purpose. It is a Level A requirement — the most fundamental level of accessibility compliance.
How to Fix It
Add an accessible name to any element with role="img" using one of these methods:
-
aria-label: Provide the text alternative directly as an attribute value. -
aria-labelledby: Reference theidof another element that contains the descriptive text. -
title: Use thetitleattribute as a fallback (thougharia-labelandaria-labelledbyare preferred, astitlehas inconsistent support across assistive technologies).
The text alternative should be concise and describe the content or purpose of the image. If the image is purely decorative and conveys no information, use aria-hidden="true" instead of role="img" to hide it from assistive technologies entirely.
Examples
Incorrect: role="img" with No Text Alternative
<div role="img">
<!-- SVG icon with no accessible name -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2L2 22h20L12 2z"/>
</svg>
</div>
A screen reader encounters this as an image but has no name to announce, so the user gets no useful information.
Correct: Using aria-label
<div role="img" aria-label="Warning: hazardous area ahead">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2L2 22h20L12 2z"/>
</svg>
</div>
Correct: Using aria-labelledby
<span id="chart-desc">Quarterly revenue growth from Q1 to Q4 2024</span>
<div role="img" aria-labelledby="chart-desc">
<!-- Complex chart composed of multiple elements -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
<rect x="10" y="60" width="30" height="40" fill="#4a90d9"/>
<rect x="50" y="40" width="30" height="60" fill="#4a90d9"/>
<rect x="90" y="25" width="30" height="75" fill="#4a90d9"/>
<rect x="130" y="10" width="30" height="90" fill="#4a90d9"/>
</svg>
</div>
Correct: Using title
<div role="img" title="Company logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="#333"/>
</svg>
</div>
Correct: Decorative Image Hidden from Assistive Technology
If the image is purely decorative and adds no meaningful information, hide it instead of labeling it:
<div aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 10">
<line x1="0" y1="5" x2="100" y2="5" stroke="#ccc" stroke-width="1"/>
</svg>
</div>
Note that role="img" is removed here because the element is not intended to be perceived as an image at all.
Scrollable regions are created when a container element has CSS overflow set to scroll, auto, or similar values, and its content exceeds the container’s visible dimensions. Mouse users can simply scroll these regions with their scroll wheel or by clicking and dragging the scrollbar. However, keyboard-only users need to be able to focus the scrollable region before they can scroll it with arrow keys or other keyboard controls.
Not all browsers automatically place scrollable regions in the tab order. Safari, notably, does not add scrollable <div> elements to the keyboard focus sequence unless they are explicitly made focusable or contain a focusable child. This means that without intervention, keyboard users in affected browsers are completely locked out of the scrollable content — they can see it but have no way to access it.
Who is affected
This issue has a serious impact on several groups of users:
- Keyboard-only users (including people with motor disabilities) cannot scroll to content hidden within the overflow region.
- Blind and deafblind users who rely on screen readers and keyboard navigation may be unable to reach or read content inside the scrollable area.
- Users of alternative input devices that emulate keyboard interaction are similarly blocked.
Related WCAG success criteria
This rule relates to the following WCAG 2.0 / 2.1 / 2.2 Level A success criteria:
- 2.1.1 Keyboard — All functionality must be operable through a keyboard interface without requiring specific timings for individual keystrokes.
- 2.1.3 Keyboard (No Exception) — All functionality must be operable through a keyboard with no exceptions.
Both criteria require that every interactive or content-bearing part of a page be fully usable via keyboard alone.
How to fix it
The most reliable approach is to make the scrollable region itself focusable by adding tabindex="0" to it. This ensures the element appears in the tab order and can receive keyboard focus, allowing the user to scroll with arrow keys.
Alternatively, you can ensure the scrollable region contains at least one focusable element (such as a link, button, or an element with tabindex="0"). If a child element can receive focus, keyboard users can tab into the scrollable region and then use arrow keys or other keyboard shortcuts to scroll.
Important: If the scrollable region contains interactive elements like <input>, <select>, or <textarea>, but all of them have tabindex="-1", the region has no keyboard-accessible entry point and will fail this rule. Either remove the negative tabindex from at least one child, or add tabindex="0" to the scrollable container itself.
When you make a scrollable container focusable, also consider adding an accessible name via aria-label or aria-labelledby so screen reader users understand the purpose of the focusable region. You may also want to add role="region" to help convey its purpose.
Examples
Failing: scrollable region with no focusable elements
All interactive children have tabindex="-1", and the container itself is not focusable. Keyboard users cannot reach or scroll this content.
<div style="height: 100px; overflow: auto;">
<input type="text" tabindex="-1" />
<select tabindex="-1"></select>
<p style="height: 500px;">
This content overflows but is unreachable by keyboard.
</p>
</div>
Failing: scrollable region with only static content
The container has overflow but no focusable element inside, and no tabindex on the container.
<div style="height: 100px; overflow-y: auto;">
<p style="height: 500px;">
Long content that requires scrolling to read fully.
</p>
</div>
Passing: focusable scrollable container
Adding tabindex="0" to the scrollable container allows keyboard users to focus it and scroll with arrow keys. An aria-label and role="region" provide context for screen reader users.
<div
style="height: 100px; overflow-y: auto;"
tabindex="0"
role="region"
aria-label="Scrollable content"
>
<p style="height: 500px;">
Long content that requires scrolling to read fully.
</p>
</div>
Passing: focusable child inside scrollable region
If you prefer not to make the container focusable, ensure at least one child element inside it can receive focus.
<div style="height: 100px; overflow-y: auto;">
<a href="#section1">Jump to section</a>
<p style="height: 500px;">
Long content that requires scrolling to read fully.
</p>
</div>
Conditional: interactive children without negative tabindex
If the scrollable region contains naturally focusable elements (like an <input>) without tabindex="-1", keyboard users can tab into the region. However, be aware that some browsers may intercept keyboard events for autocomplete, which can interfere with scrolling. Adding tabindex="0" directly to the scrollable container is the more robust solution.
<div style="height: 100px; overflow-y: scroll;">
<input type="text" />
<p style="height: 500px;">Additional content below the input.</p>
</div>
When a <select> element lacks an accessible name, screen readers announce it as something generic like “combobox” or “listbox” with no context about what it represents. A sighted user might see nearby text like “Country” and understand the purpose, but that visual proximity means nothing to assistive technology unless a programmatic relationship exists between the text and the form control.
This issue critically affects users who are blind, have low vision, or have mobility impairments and rely on assistive technologies to interact with forms. Without a proper label, these users cannot determine what data a dropdown expects, making form completion error-prone or impossible.
Related WCAG Success Criteria
This rule maps to WCAG 2.0, 2.1, and 2.2 Success Criterion 4.1.2: Name, Role, Value (Level A), which requires that all user interface components have a name that can be programmatically determined. It also falls under Section 508 §1194.22(n), which mandates that online forms allow people using assistive technology to access all field elements, directions, and cues needed for completion.
How to Fix It
There are several ways to give a <select> element an accessible name, listed here from most preferred to least preferred:
-
Explicit
<label>withfor/id— The most common and recommended approach. Use theforattribute on the<label>to match theidof the<select>. This also gives sighted users a larger click target. -
Implicit
<label>wrapping — Wrap the<select>inside a<label>element. Nofor/idpairing is needed. -
aria-labelledby— Point to theidof an existing visible text element that serves as the label. Useful when a traditional<label>would break layout or when multiple elements combine to form the label. -
aria-label— Provide an invisible text label directly on the<select>. Use this only when no visible label text exists or is appropriate.
Whichever method you choose, make sure:
- The label text clearly describes what the user should select.
-
Each
idattribute is unique on the page. -
Each
<select>has only one labeling method to avoid conflicts or confusion.
Examples
Incorrect: No Label Association
This places text near the <select> but creates no programmatic link between them. Screen readers will not announce “State” when the user focuses the dropdown.
State:
<select>
<option value="ny">New York</option>
<option value="ca">California</option>
</select>
Correct: Explicit <label> with for and id
The for attribute on the <label> matches the id on the <select>, creating a clear programmatic association.
<label for="state">State:</label>
<select id="state">
<option value="ny">New York</option>
<option value="ca">California</option>
</select>
Correct: Implicit <label> Wrapping
Wrapping the <select> inside the <label> element implicitly associates them.
<label>
State:
<select>
<option value="ny">New York</option>
<option value="ca">California</option>
</select>
</label>
Correct: Using aria-labelledby
When visible text already exists elsewhere (e.g., in a heading or table cell), use aria-labelledby to reference it by id.
<span id="state-label">State:</span>
<select aria-labelledby="state-label">
<option value="ny">New York</option>
<option value="ca">California</option>
</select>
Correct: Using aria-label
When no visible label is present or appropriate, aria-label provides an accessible name directly. Note that this label is invisible to sighted users, so only use it when context is already visually clear.
<select aria-label="State">
<option value="ny">New York</option>
<option value="ca">California</option>
</select>
A server-side image map is created when an <img> element includes the ismap attribute inside an <a> element. When a user clicks the image, the browser sends the x and y coordinates of the click to the server, which then determines what action to take. This approach has two critical accessibility failures:
-
No keyboard access. Because the interaction depends on mouse coordinates, there is no way for keyboard-only users to activate specific regions of the map. Keyboard users cannot generate coordinate-based clicks, so every link within the image map becomes unreachable.
-
No text alternatives for individual regions. Screen readers cannot identify or announce the distinct clickable areas within a server-side image map. Unlike client-side image maps, where each
<area>element can have analtattribute describing its purpose, server-side image maps offer no mechanism to label individual hotspots. This leaves blind and deafblind users unable to understand or interact with the content.
Users with mobility impairments who rely on switch devices or other keyboard-based assistive technologies are also affected, as these tools cannot produce the precise mouse coordinates that server-side image maps require.
This rule relates to WCAG 2.1 / 2.2 Success Criterion 2.1.1 (Keyboard) at Level A, which requires that all functionality be operable through a keyboard interface. It also aligns with Section 508 §1194.22(f), which explicitly states that client-side image maps must be provided instead of server-side image maps except where regions cannot be defined with an available geometric shape.
How to Fix
Replace every server-side image map with a client-side image map:
-
Remove the
ismapattribute from the<img>element. -
Remove the wrapping
<a>element (if it was only used for the server-side map). -
Add a
usemapattribute to the<img>element, referencing a<map>element by name. -
Define each clickable region using
<area>elements inside the<map>, specifying theshape,coords,href, andaltattributes for each one. -
Ensure every
<area>has a meaningfulaltattribute describing the link destination.
Examples
Incorrect: Server-side image map
The ismap attribute makes this a server-side image map. Keyboard users cannot access the links, and no text alternatives exist for individual regions.
<a href="/maps/nav.map">
<img src="/images/navbar.gif" alt="Navigation bar" ismap>
</a>
Correct: Client-side image map
Each clickable region is defined with an <area> element that has its own alt text and href. Keyboard users can tab through the areas, and screen readers can announce each link.
<img
src="/images/solar_system.jpg"
alt="Solar System"
width="472"
height="800"
usemap="#solar-system-map">
<map name="solar-system-map">
<area
shape="rect"
coords="115,158,276,192"
href="https://en.wikipedia.org/wiki/Mercury_(planet)"
alt="Mercury">
<area
shape="rect"
coords="115,193,276,234"
href="https://en.wikipedia.org/wiki/Venus"
alt="Venus">
<area
shape="rect"
coords="115,235,276,280"
href="https://en.wikipedia.org/wiki/Earth"
alt="Earth">
</map>
Correct: Simple navigation using a client-side image map
<img
src="/images/navbar.gif"
alt="Site navigation"
width="600"
height="50"
usemap="#nav-map">
<map name="nav-map">
<area
shape="rect"
coords="0,0,150,50"
href="/home"
alt="Home">
<area
shape="rect"
coords="150,0,300,50"
href="/products"
alt="Products">
<area
shape="rect"
coords="300,0,450,50"
href="/about"
alt="About us">
<area
shape="rect"
coords="450,0,600,50"
href="/contact"
alt="Contact">
</map>
In the client-side examples, each <area> is focusable via keyboard, has a descriptive alt attribute for screen readers, and links directly to its destination without requiring server-side coordinate processing. If your image map regions are complex and cannot be represented with the available shape values (rect, circle, poly), consider replacing the image map entirely with a set of individual links or buttons, which provide even better accessibility.
Screen readers and keyboard-only navigation process page content sequentially, following the DOM order. This means that everything before the main content — site logos, navigation bars, search forms, utility links — must be traversed before a user reaches what they actually came for. On complex sites, this repetitive content can be extremely lengthy.
A properly functioning skip link gives these users a shortcut. When the skip link’s target is missing or not focusable, activating the link does nothing. The user remains at the top of the page with no indication of what went wrong, which is both confusing and frustrating.
This issue primarily affects:
- Blind and deafblind users who rely on screen readers to navigate sequentially.
- Keyboard-only users (including those with mobility impairments) who tab through every interactive element.
- Sighted keyboard users who benefit from the visual skip link appearing on focus.
This rule is a Deque best practice and aligns with WCAG technique G1 (“Adding a link at the top of each page that goes directly to the main content area”), which supports WCAG 2.4.1 Bypass Blocks (Level A). Success Criterion 2.4.1 requires that a mechanism exists to bypass blocks of content repeated across multiple pages.
How to Fix It
-
Add a skip link as the very first focusable element inside the
<body>tag, before any navigation or repeated content. -
Set the
hrefto a fragment identifier (e.g.,#main-content) that matches theidof the target element. -
Ensure the target element exists in the DOM with the corresponding
id. -
Make the target focusable if it is not natively focusable. Non-interactive elements like
<div>or<main>needtabindex="-1"so the browser can move focus to them when the skip link is activated. -
Do not hide the skip link with
display: noneorvisibility: hidden, as these make the link inaccessible to all users, including screen reader users.
Handling the Skip Link’s Visibility
You can make the skip link visually hidden until it receives keyboard focus. This approach keeps the layout clean for mouse users while remaining fully accessible to keyboard users:
- Use CSS to position the link off-screen by default.
-
On
:focus, bring it back on-screen so sighted keyboard users can see it.
Alternatively, you can make the skip link permanently visible — this is the simplest and most inclusive approach.
Important: Never use display: none or visibility: hidden on the skip link. These properties remove the element from the accessibility tree and the tab order, making it useless for everyone.
Examples
Incorrect: Skip Link Target Does Not Exist
The skip link points to #main-content, but no element with that id exists on the page.
<body>
<a href="#main-content">Skip to main content</a>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main>
<h1>Welcome</h1>
<p>Page content goes here.</p>
</main>
</body>
Incorrect: Target Exists but Is Not Focusable in Some Browsers
Some older or WebKit-based browsers may not move focus to a non-interactive element even with a matching id, unless tabindex="-1" is added.
<body>
<a href="#main-content">Skip to main content</a>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<div id="main-content">
<h1>Welcome</h1>
<p>Page content goes here.</p>
</div>
</body>
Correct: Skip Link with a Focusable Target
The target element has both a matching id and tabindex="-1" to ensure it receives focus reliably across all browsers.
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main id="main-content" tabindex="-1">
<h1>Welcome</h1>
<p>Page content goes here.</p>
</main>
</body>
Correct: Skip Link That Is Visually Hidden Until Focused
This CSS pattern hides the skip link off-screen by default and reveals it when the user tabs to it.
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main id="main-content" tabindex="-1">
<h1>Welcome</h1>
<p>Page content goes here.</p>
</main>
</body>
.skip-link {
position: absolute;
left: -9999px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: static;
width: auto;
height: auto;
overflow: visible;
padding: 8px 16px;
background: #ffc;
border: 2px solid #990000;
z-index: 1000;
}
What the Rule Checks
This axe-core rule (skip-link) verifies that every skip link on the page — typically the first link in the document — has a target that both exists in the DOM and is focusable. If the href points to a fragment identifier like #main-content, the rule confirms that an element with id="main-content" is present and can receive focus.
When an <svg> element is given a semantic role like img, graphics-document, or graphics-symbol, it signals to assistive technologies that the element conveys meaningful content — just like an <img> tag would. However, unlike <img> elements that have a straightforward alt attribute, SVG elements require different techniques to provide their accessible name. If no text alternative is supplied, screen reader users will encounter the SVG as an unlabeled graphic, losing access to whatever information it communicates.
This issue affects people who are blind, deafblind, or who use screen readers for other reasons. It can also impact users of braille displays and other assistive technologies that rely on programmatically determined text to represent non-text content.
Related WCAG Success Criteria
This rule maps to WCAG Success Criterion 1.1.1: Non-text Content (Level A), which requires that all non-text content presented to the user has a text alternative that serves the equivalent purpose. This criterion applies across WCAG 2.0, 2.1, and 2.2, and is also required by Section 508 and EN 301 549.
Because this is a Level A requirement, it represents the most fundamental level of accessibility. Failing to meet it means some users will have no way to understand the content your SVG conveys.
How to Fix It
There are several ways to give an <svg> element an accessible text alternative. Use one of the following approaches:
Use a <title> child element
The <title> element must be a direct child of the <svg> element. It provides a short text description that assistive technologies can read. If there are multiple <title> children, the first one must contain text — only the first <title> is used.
Use the aria-label attribute
Add an aria-label attribute directly to the <svg> element with a concise description of the graphic’s content.
Use the aria-labelledby attribute
Reference one or more existing elements on the page by their id values using aria-labelledby. This is especially useful when the description text is already visible elsewhere on the page.
Use the title attribute
The title attribute on the <svg> element can also provide an accessible name, though <title> child elements or ARIA attributes are generally preferred for better screen reader support.
How the Rule Works
The axe rule checks that the <svg> element with a role of img, graphics-document, or graphics-symbol has a computable accessible name. Specifically, it looks for:
-
A direct
<title>child element with non-empty text content -
An
aria-labelattribute with a non-empty value -
An
aria-labelledbyattribute that references elements with text content
The check fails if:
-
There is no
<title>child and no ARIA labeling attribute -
The
<title>child is empty or contains only whitespace -
The
<title>element is a grandchild (nested inside another element within the SVG) rather than a direct child -
There are multiple
<title>elements and the first one is empty
Examples
Failing — SVG with role="img" and no text alternative
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
Screen readers will announce this as an image but cannot describe its content.
Failing — Empty <title> element
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<title></title>
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
Failing — <title> is a grandchild, not a direct child
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g>
<title>A blue circle</title>
<circle cx="50" cy="50" r="40" fill="blue" />
</g>
</svg>
The <title> must be a direct child of the <svg> element itself.
Failing — First <title> is empty
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<title></title>
<title>A blue circle</title>
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
Only the first <title> child is evaluated. Since it is empty, the rule fails even though the second one has text.
Passing — Using a <title> child element
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<title>A blue circle</title>
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
Passing — Using aria-label
<svg role="img" aria-label="A blue circle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
Passing — Using aria-labelledby
<h2 id="chart-heading">Monthly Sales Data</h2>
<svg role="img" aria-labelledby="chart-heading" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
<rect x="10" y="50" width="30" height="50" fill="green" />
<rect x="50" y="20" width="30" height="80" fill="green" />
</svg>
Passing — Decorative SVG excluded from the accessibility tree
If an SVG is purely decorative and conveys no meaning, you can hide it from assistive technologies instead of adding a text alternative:
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
Note that this approach removes the role="img" and adds aria-hidden="true", so the rule no longer applies. Only use this for truly decorative graphics — if the SVG conveys any information, it needs a text alternative.
Screen readers announce both the <caption> and summary when a user encounters a data table. The <caption> element serves as the table’s visible, on-screen title — it tells all users what the table is about. The summary attribute (available in HTML4 and XHTML, though deprecated in HTML5) is read only by screen readers and is intended to describe the table’s structure, such as how rows and columns are organized. When these two contain the same text, the screen reader simply reads the same content twice, which provides no additional value and creates a confusing, redundant experience.
This issue primarily affects blind and deafblind users who rely on screen readers to navigate and interpret tabular data. Screen readers have specialized table navigation features, but they depend on accurate, meaningful markup to function properly. When the caption and summary are duplicated, users lose the opportunity to get both a concise title and a helpful structural description of the table.
This rule is identified as a Deque best practice and is also referenced in the RGAA (French government accessibility standard). While it doesn’t map directly to a specific WCAG success criterion, it supports the principles behind WCAG 1.3.1 Info and Relationships (Level A), which requires that information and structure conveyed visually are also available programmatically.
How to Fix It
The fix is straightforward: make sure the <caption> and summary attribute contain different text that each serves its intended purpose.
-
Use the
<caption>element to give the table a concise, visible title that describes what data the table contains. -
Use the
summaryattribute to describe the table’s structure — for example, how many column groups there are, what the row and column headers represent, or how the data is organized.
If you find you can’t think of distinct content for both, consider whether you actually need the summary attribute at all. For simple tables, a <caption> alone is often sufficient. Note that summary is deprecated in HTML5, so for modern documents you may want to provide structural descriptions using other techniques, such as a paragraph linked with aria-describedby.
Examples
Incorrect: Identical Caption and Summary
The summary and <caption> contain the same text, causing screen readers to repeat the information.
<table summary="Quarterly sales figures">
<caption>Quarterly sales figures</caption>
<tr>
<th>Quarter</th>
<th>Revenue</th>
</tr>
<tr>
<td>Q1</td>
<td>$50,000</td>
</tr>
<tr>
<td>Q2</td>
<td>$62,000</td>
</tr>
</table>
Correct: Caption and Summary Provide Different Information
The <caption> names the table, while the summary describes its structure.
<table summary="Column 1 lists each quarter, column 2 shows total revenue for that quarter.">
<caption>Quarterly sales figures</caption>
<tr>
<th>Quarter</th>
<th>Revenue</th>
</tr>
<tr>
<td>Q1</td>
<td>$50,000</td>
</tr>
<tr>
<td>Q2</td>
<td>$62,000</td>
</tr>
</table>
Correct: Using Only a Caption (HTML5 Approach)
For simple tables in HTML5, you can skip the deprecated summary attribute entirely and rely on a <caption> alone.
<table>
<caption>Quarterly sales figures</caption>
<tr>
<th>Quarter</th>
<th>Revenue</th>
</tr>
<tr>
<td>Q1</td>
<td>$50,000</td>
</tr>
<tr>
<td>Q2</td>
<td>$62,000</td>
</tr>
</table>
Correct: HTML5 Alternative Using aria-describedby
If you need to provide a structural description in HTML5, use aria-describedby to link to a nearby paragraph instead of using summary.
<p id="table-desc">Column 1 lists each quarter, column 2 shows total revenue for that quarter.</p>
<table aria-describedby="table-desc">
<caption>Quarterly sales figures</caption>
<tr>
<th>Quarter</th>
<th>Revenue</th>
</tr>
<tr>
<td>Q1</td>
<td>$50,000</td>
</tr>
<tr>
<td>Q2</td>
<td>$62,000</td>
</tr>
</table>
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.
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.
Captions are the primary way deaf and hard-of-hearing users access the audio portion of video content. When a video lacks captions, these users can see the visual content but miss everything communicated through sound — including spoken dialogue, narration, music, ambient sounds, and sound effects that provide context or meaning. This creates a critical barrier to understanding.
This rule relates to WCAG 2.0/2.1/2.2 Success Criterion 1.2.2: Captions (Prerecorded) at Level A, which requires that captions be provided for all prerecorded audio content in synchronized media. It is also required under Section 508 and EN 301 549. Because this is a Level A requirement, it represents the minimum baseline for accessibility — failing to provide captions is one of the most impactful accessibility issues a video can have.
Captions vs. Subtitles
It’s important to understand the difference between captions and subtitles, as they serve different purposes:
-
Captions (
kind="captions") are designed for deaf and hard-of-hearing users. They include all dialogue plus descriptions of meaningful non-speech audio such as sound effects, music, speaker identification, and other auditory cues (e.g., “[door slams]”, “[dramatic orchestral music]”, “[audience applause]”). -
Subtitles (
kind="subtitles") are language translations of dialogue and narration, intended for hearing users who don’t understand the spoken language. Subtitles generally do not include non-speech audio descriptions.
For accessibility compliance, you must use kind="captions", not kind="subtitles".
What Makes Good Captions
Good captions go beyond transcribing dialogue. They should:
- Identify who is speaking when it’s not visually obvious
- Include meaningful sound effects (e.g., “[phone ringing]”, “[glass shattering]”)
- Describe music when it’s relevant (e.g., “[soft piano music]”, “[upbeat pop song playing]”)
- Note significant silence or pauses when they carry meaning
- Be accurately synchronized with the audio
- Use proper spelling, grammar, and punctuation
How to Fix the Problem
Add a <track> element inside your <video> element with the following attributes:
-
src— the URL of the caption file (typically in WebVTT.vttformat) -
kind— set to"captions" -
srclang— the language code of the captions (e.g.,"en"for English) -
label— a human-readable label for the track (e.g.,"English")
Only src is technically required, but kind, srclang, and label are strongly recommended for clarity and to ensure assistive technologies and browsers handle the track correctly.
Examples
Incorrect: Video with no captions
<video width="640" height="360" controls>
<source src="presentation.mp4" type="video/mp4">
</video>
This video has no <track> element, so deaf and hard-of-hearing users cannot access any of the audio content.
Incorrect: Using subtitles instead of captions
<video width="640" height="360" controls>
<source src="presentation.mp4" type="video/mp4">
<track src="subs_en.vtt" kind="subtitles" srclang="en" label="English">
</video>
While this provides a text track, kind="subtitles" does not satisfy the captions requirement. Subtitles typically include only dialogue and won’t convey non-speech audio information.
Correct: Video with captions
<video width="640" height="360" controls>
<source src="presentation.mp4" type="video/mp4">
<track src="captions_en.vtt" kind="captions" srclang="en" label="English">
</video>
Correct: Video with captions in multiple languages
<video width="640" height="360" controls>
<source src="presentation.mp4" type="video/mp4">
<track src="captions_en.vtt" kind="captions" srclang="en" label="English" default>
<track src="captions_es.vtt" kind="captions" srclang="es" label="Español">
</video>
The default attribute indicates which caption track should be active by default when the user has captions enabled.
Example WebVTT Caption File
A basic captions_en.vtt file looks like this:
WEBVTT
00:00:01.000 --> 00:00:04.000
[upbeat music playing]
00:00:05.000 --> 00:00:08.000
Sarah: Welcome to our annual conference!
00:00:09.000 --> 00:00:12.000
[audience applause]
00:00:13.000 --> 00:00:17.000
Sarah: Today we'll explore three key topics.
Notice how the captions identify the speaker, describe non-speech sounds, and are synchronized to specific timestamps.
Pronto para validar os seus sites?
Comece o seu teste gratuito hoje.