Français

ARIA, the scalpel of web accessibility

Table of contents

A precision tool

The WebAIM Million 2026 report is unequivocal: pages that use ARIA have more accessibility errors than those that don't. I explain why in ARIA, the poison of web accessibility, along with the four rules the W3C laid down to limit the damage.

But ARIA isn't a poison by nature. It's a scalpel: essential in the right hands, dangerous in the wrong ones. HTML has real limitations, and when you reach them, ARIA is the only answer.

This article will show you when ARIA is the right tool, which anti-patterns to avoid at all costs, and how to test in depth with a screen reader.

When ARIA is the right answer

If the W3C's first rule is "don't use ARIA", it's because ARIA exists for cases where HTML isn't enough. And those cases exist. Not on every page, but when you encounter them, ARIA is the only answer.

Content that changes without warning

A success message that appears after submitting a form. A cart counter that updates. A connection error notification. A chat message. In all these cases, content changes visually, but the screen reader doesn't know. It reads the page as it was on load. Without instructions to the contrary, the new content is invisible.

This is the problem solved by live regions — the only mechanism that lets you tell the screen reader "something changed, announce it." Sara Soueidan details every nuance in her reference series on the subject.

live-regions.html
<!-- Non-urgent confirmation — the screen reader
     waits for a pause in its reading -->
<p aria-live="polite">
  Item added to cart (3 items).
</p>
 
<!-- Critical error — the screen reader
     immediately interrupts its reading -->
<p aria-live="assertive">
  Connection lost. Your changes are not saved.
</p>

aria-live="polite" is a discreet notification: the screen reader waits until the user is available before announcing the change. aria-live="assertive" is an interruption: it cuts off what the screen reader is currently saying to announce the message immediately. The rule is simple: if the user can wait, use polite. If the information is critical and urgent, use assertive.

The <output> element carries an implicit status role with a polite live region, but it's designed for calculation results, not generic notifications. For the general case — from action confirmations to error alerts — no native HTML element covers the need. This is a case where ARIA is the only answer.

Widgets that HTML doesn't have

HTML has buttons, links, checkboxes, text fields, dropdown lists. But it doesn't have tabs, expandable tree views, or rich comboboxes with filtering and groups.

The GOV.UK Design System and Heydon Pickering in Inclusive Components show how to build accessible tabs from a list of internal links: a <ul> with <li> elements and <a href="#section"> links. Without JavaScript, it's a functional table of contents. With JavaScript, the list becomes a tab group:

tabs-progressive.html
<!-- Without JavaScript: a table of contents -->
<ul>
  <li><a href="#general">General</a></li>
  <li><a href="#security">Security</a></li>
  <li><a href="#notifications">Notifications</a></li>
</ul>
 
<!-- With JavaScript: ARIA roles
     transform the semantics -->
<ul role="tablist">
  <li role="presentation">
    <a role="tab" href="#general" id="tab-general"
       aria-controls="general" aria-selected="true">General</a>
  </li>
  <li role="presentation">
    <a role="tab" href="#security" id="tab-security"
       aria-controls="security" aria-selected="false"
       tabindex="-1">Security</a>
  </li>
  <li role="presentation">
    <a role="tab" href="#notifications" id="tab-notifications"
       aria-controls="notifications" aria-selected="false"
       tabindex="-1">Notifications</a>
  </li>
</ul>
 
<section id="general" role="tabpanel" aria-labelledby="tab-general">
  <!-- ... -->
</section>

The <ul> becomes a tablist, the <a> elements become tabs — so far, the semantics evolve with the behavior. But the <li> elements pose a problem: in a tablist, there's no "list of items". If left as they are, the screen reader announces "list, 3 items, tab, General" — a mix of two semantics that makes no sense. role="presentation" erases the list item semantics without removing the element from the DOM — this is exactly the case covered by ARIA's second rule: modifying native semantics when they conflict with the element's actual role.

The markup only takes you halfway. Each <a> points to its panel via aria-controls, each panel receives role="tabpanel" and aria-labelledby, and JavaScript still has to provide the keyboard layer: left/right arrows to move between tabs (the tabindex="-1" takes inactive tabs out of the Tab order, that's the roving tabindex from the APG pattern), toggling aria-selected, hiding inactive panels. Without that layer, the component announces "tab" but behaves like a list of links: the role's promise isn't kept.

The same principle applies to tree views (a keyboard-navigable hierarchy) or rich comboboxes (a field with filtering, groups and keyboard navigation that <datalist> doesn't cover). In all these cases, ARIA doesn't replace HTML — it complements it.

States that HTML can't express

A dropdown menu is open or closed. A link points to the current page. A field is associated with a help message. HTML can't express these states — ARIA does:

aria-states.html
<!-- The button announces "Menu, collapsed" or
     "Menu, expanded" -->
<button aria-expanded="false"
        aria-controls="menu-navigation">
  Menu
</button>
<ul id="menu-navigation" hidden>
  <li><a href="/products">Products</a></li>
  <li><a href="/contact">Contact</a></li>
</ul>
 
<!-- The screen reader announces
     "Home, link, current page" -->
<nav aria-label="Main navigation">
  <a href="/" aria-current="page">Home</a>
  <a href="/products">Products</a>
  <a href="/contact">Contact</a>
</nav>
 
<!-- The help message is read after the field -->
<label for="email">Email address</label>
<input id="email" type="email"
       aria-describedby="email-help">
<p id="email-help">
  Used only for order confirmation.
</p>

aria-expanded transforms a silent button into one that announces its state. aria-current="page" tells the user where they are in the navigation — without it, all links look the same. aria-describedby associates help text with a field without touching its label: the screen reader reads the label first, then the supplementary description.

No native HTML attribute does this work. This is ARIA in its exact role: expressing what HTML cannot say.

The anti-patterns that poison

You now know when ARIA is the right answer. Léonie Watson navigates the web with a screen reader eight hours a day, and what she hears most often are the same errors coming back. Here are the most widespread.

The name that contradicts the label

label-mismatch.html
<!-- ❌ The button displays "Search" but
     the screen reader announces "Find a product" -->
<button aria-label="Find a product">
  Search
</button>

The user navigating by voice sees the word "Search" and says "click Search". Nothing happens — because the button's accessible name is "Find a product", not "Search". The aria-label replaced the visible text in the accessibility tree.

WCAG 2.5.3 Label in Name (Level A) forbids it: the visible label must be contained in the accessible name. If you want to add context, use aria-describedby — it supplements the name without replacing it:

label-match.html
<!-- ✅ The visible text is the accessible name,
     additional context is in the description -->
<button aria-describedby="search-help">
  Search
</button>
<p id="search-help" hidden>
  In the product catalog
</p>

Content that's made to disappear

aria-hidden-danger.html
<!-- ❌ The out-of-stock product is grayed out visually,
     the dev also hides it from the screen reader
     — but the link remains focusable -->
<article class="product sold-out"
         aria-hidden="true">
  <h3>Wireless headphones</h3>
  <p>$89.99 — <strong>Out of stock</strong></p>
  <a href="/product/wireless-headphones">View product</a>
</article>

The developer grayed out the out-of-stock product and thought it logical to also hide it from the screen reader. But the information is still useful: the price, availability, the link to the product page. And above all, aria-hidden="true" is inherited by all descendants: the link disappears from the accessibility tree but remains focusable via keyboard. The user lands on a mute element — no name, no destination.

Visually dimmed doesn't mean useless. If the content should remain on the page, it should remain in the accessibility tree:

aria-hidden-fix.html
<!-- ✅ The product remains accessible,
     its state is announced clearly -->
<article class="product sold-out">
  <h3>Wireless headphones</h3>
  <p>$89.99 — <strong>Out of stock</strong></p>
  <a href="/product/wireless-headphones">View product</a>
</article>
 
<!-- ✅ If the content truly needs to disappear,
     hidden removes it from rendering AND the
     accessibility tree -->
<article class="product sold-out" hidden>
  <h3>Wireless headphones</h3>
  <p>$89.99 — <strong>Out of stock</strong></p>
  <a href="/product/wireless-headphones">View product</a>
</article>

Roles that serve no purpose

redundant-roles.html
<!-- ❌ Redundant — these HTML elements already
     carry these roles implicitly -->
<nav role="navigation">...</nav>
<main role="main">...</main>
<header role="banner">...</header>
<footer role="contentinfo">...</footer>
<button role="button">Submit</button>
<a role="link" href="/contact">Contact</a>

A <nav> is a navigation. A <main> is a main. A <button> is a button. Adding the explicit role on an element that already carries it is like writing color: red; color: red; in CSS: it doesn't break anything, but it signals the developer doesn't know what the element does natively. And this noise masks real problems in an audit.

The aria-label nobody hears

aria-label-static.html
<!-- ❌ aria-label on a paragraph —
     ignored by most screen readers -->
<p aria-label="Important warning">
  This product contains allergens.
</p>

aria-label is not a universal attribute. Ben Myers documented it in detail: the ARIA specification only allows it on elements whose role accepts a name (widgets, landmarks, images) — not on static elements like <p>, <span> or <div> without a role. And an explicit role isn't always enough: some roles like presentation or generic also prohibit naming ("Name from: prohibited" in the spec). In practice, most screen readers ignore it on these elements. The paragraph text is read normally, the aria-label is lost.

If the visible text isn't enough, the solution isn't an invisible aria-label — it's better visible text. I show a concrete example in Anatomy of a product card, where each aria-label is weighed case by case:

better-text.html
<!-- ✅ The text says what it needs to say,
     for everyone -->
<p><strong>Warning:</strong>
  this product contains allergens.</p>

The button with no name

accessible-name.html
<!-- ❌ No accessible name — the screen reader
     announces just "button" -->
<button>
  <svg viewBox="0 0 24 24"><!-- icon --></svg>
</button>
 
<!-- ✅ aria-label when the icon alone
     suffices visually -->
<button aria-label="Close the modal">
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <!-- icon -->
  </svg>
</button>
 
<!-- ✅✅ Even better: visible text
      next to the icon -->
<button>
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <!-- icon -->
  </svg>
  Close
</button>

A button with just an SVG icon, with no text and no aria-label, is announced as "button" — with no indication of what it does. aria-label fixes the problem for screen readers, but not for people who navigate by voice: they see an icon and don't know which word to speak to activate the button. Visible text is the most robust solution: it serves as the accessible name, a reference point for voice navigation, and a fallback when CSS doesn't load.

Testing in depth

The three 30-second checks from the previous article use the accessibility tree in the developer tools. That's a good first filter. But to test what the user actually hears, you need to activate a screen reader.

VoiceOver on macOS

Activate VoiceOver with Cmd + F5 (Fn + Cmd + F5 on recent keyboards, or a Touch ID triple-press). Navigate your page with Tab (interactive elements) and Ctrl + Option + arrows (sequential reading). Listen to what the screen reader announces for each element: the role, the name, the state.

The rotor (Ctrl + Option + U) is your structural audit tool. It displays the list of headings, links, landmarks and form controls. If an element is missing from the rotor when it should be there, or if it appears with an incomprehensible name, your ARIA has a problem.

Other screen readers

VoiceOver isn't the only tool, and your users aren't all on macOS:

  • NVDA + Firefox on Windows: free and open source, the most widely used by developers on Windows. Navigate with Tab (interactive elements) and arrows (sequential reading). The elements list (Insert + F7) plays the same role as the rotor.
  • JAWS + Chrome or Edge on Windows: the professional reference, paid. Same navigation logic, with the links list (Insert + F7) and the headings list (Insert + F6).
  • TalkBack + Chrome on Android: the mobile screen reader. Navigate by swiping (left/right for sequential, up/down to change navigation mode). The reading menu (swipe down then right) gives access to headings, links and controls.

Each screen reader has its quirks, but the warning signs remain the same.

What to listen for

Four warning signs:

  1. A role announced without behavior. The screen reader says "button" but Enter does nothing. You have a role="button" without a keyboard handler.

  2. A name that doesn't match. The screen reader announces "Find a product" but you read "Search" on screen. You have an aria-label that contradicts the visible text.

  3. Silence where you expect content. You navigate with Tab and focus stops on a mute element. You have an aria-hidden="true" on a focusable element.

  4. An announcement that never comes. You submit a form, a success message appears, but the screen reader says nothing. You're missing an aria-live on the notification area.

Each of these signs corresponds to an anti-pattern from the previous section. The screen reader only reveals what the accessibility tree already contained.

The overall criteria

Every case you've just seen — live regions, tabs, states, anti-patterns — WCAG translates them into verifiable criteria:

All these criteria converge on the same idea: every role placed is a commitment — the component must behave as that role promises. Every accessible name is a contract — it must match what the user sees and can speak. The accessibility tree and the screen reader tell you whether you're keeping it.

Open your page, launch VoiceOver or NVDA, and listen. Thirty seconds is all it takes to hear what the screen reader announces.

You have the scalpel. Now use it.

Sources