Avoiding Common ARIA Mistakes (The First Rule of ARIA)
AI generated
A11Y
WCAG
Accessibility · ARIA · WCAG · Screen readers
Avoiding Common ARIA Mistakes
The first rule of ARIA in practice

ARIA applied incorrectly often makes web pages less accessible rather than more. This article walks through the most common ARIA mistakes in real frontends, from redundant roles to misused aria-hidden and missing required states, and delivers a practical before-and-after audit for Magento and Hyvä projects.

17 min read ARIA · Roles · States · Live Regions WCAG 2.2 · Hyvä Theme · Alpine.js

1. The first rule of ARIA: no ARIA is better than bad ARIA

The first rule of ARIA, laid out in the official WAI-ARIA Authoring Practices Guide, essentially says: if a native HTML element or attribute already provides the desired semantics and interaction, use it instead of repurposing a generic element and retrofitting it with ARIA roles. That sounds trivial, yet it gets ignored constantly in practice. Developers reach for ARIA because it feels powerful, and many tutorials present it as a universal solution, when in reality ARIA is purely a translation layer for the operating system's accessibility API. It changes neither the appearance nor the native keyboard behavior of an element.

That is exactly where the danger lies: ARIA can tell a screen reader something that the page's actual behavior does not back up. A <div role="button"> without a keyboard handler announces itself as a button but does not respond to Enter or Space. For screen reader users that is worse than no ARIA at all, because the announcement creates an expectation the page then fails to meet. Each of the following sections covers one concrete failure pattern that shows up regularly in real Magento and Hyvä frontends, along with a clear fix.

2. Redundant roles on native elements

Arguably the most common ARIA mistake is the redundant role: <button role="button">, <nav role="navigation">, or <input type="checkbox" role="checkbox">. Native HTML elements already carry their implicit ARIA role in the browser's accessibility tree. An extra role attribute adds no information but does raise the risk of conflicting state once code changes later and the attribute is not kept in sync. Static analysis tools such as eslint-plugin-jsx-a11y now flag these redundant roles as a warning by default.

It gets worse when a role actually overrides native semantics and changes them, for example <h2 role="presentation">, which removes the heading entirely from the document structure for screen reader users. In Magento templates this often happens unintentionally, when a CMS block snippet is copied from a design template without a second thought. The audit rule is simple: every role attribute on a native element is a candidate for removal, unless it deliberately shifts semantics in an intended direction, such as role="presentation" on a purely layout-driven table.


<!-- WRONG: redundant roles duplicate native semantics -->
<button role="button" type="submit">Save</button>
<nav role="navigation" aria-label="Main menu">...</nav>
<input type="checkbox" role="checkbox" id="newsletter">
<h3 role="heading" aria-level="3">Product details</h3>

<!-- RIGHT: native elements already expose the correct role -->
<button type="submit">Save</button>
<nav aria-label="Main menu">...</nav>
<input type="checkbox" id="newsletter">
<h3>Product details</h3>

<!-- Exception: role is justified when it deliberately changes semantics -->
<table role="presentation">
  <!-- purely layout table, not tabular data -->
  <tr><td><img src="logo.svg" alt="Mironsoft"></td></tr>
</table>

3. aria-hidden on focusable content

aria-hidden="true" removes an element entirely from the accessibility tree, so screen readers skip it. That is useful for purely decorative icons or duplicated content. The classic ARIA mistake happens when aria-hidden is set on a container that holds a focusable element, such as a link, button, or form field. The element disappears from the screen reader's output but remains reachable via Tab. A screen reader user then lands on an element that stays completely silent, with no announcement at all, which is deeply confusing and counts as a violation of WCAG success criterion 4.1.2 (Name, Role, Value).

This pattern typically shows up in closed mobile menus, hidden modal backgrounds, or carousels whose off-screen slides remain in the DOM. The correct fix depends on the purpose: for regions removed from interaction entirely, the native inert attribute is the more robust choice, since it blocks keyboard focus in addition to the accessibility tree. For elements that are only visually hidden but need to reappear later, aria-hidden must always be paired with tabindex="-1" on every focusable descendant, or better still: the element is removed from the DOM entirely while it is invisible.


<!-- WRONG: focusable link stays tabbable while hidden from screen readers -->
<div aria-hidden="true" class="hidden md:block">
  <a href="/contact">Contact us</a>
</div>

<!-- RIGHT (still in DOM, needs to reappear later): remove from both trees -->
<div aria-hidden="true" class="hidden md:block">
  <a href="/contact" tabindex="-1">Contact us</a>
</div>

<!-- BETTER: use inert for fully disabled regions (blocks focus AND a11y tree) -->
<div inert class="mobile-menu-closed">
  <a href="/contact">Contact us</a>
</div>

<!-- BEST for content that toggles: don't render it at all when hidden -->
<template x-if="mobileMenuOpen">
  <div>
    <a href="/contact">Contact us</a>
  </div>
</template>

4. Missing required states and properties

Every ARIA role in the WAI-ARIA specification defines not just semantics but also a list of required and supported states and properties. role="checkbox" mandates aria-checked, role="tab" mandates aria-selected, and a combobox pattern mandates aria-expanded and aria-controls. If the role is set but the required state is forgotten, the screen reader correctly announces the element as a checkbox or tab, but without any state, which is meaningless to users. This half-finished ARIA is the second most common ARIA mistake after the redundant role.

This is especially critical for dynamic states that change via JavaScript but are only set once in the markup. An accordion header with aria-expanded="false" whose value never updates via x-bind or Alpine reactivity when opened remains permanently marked as closed for the screen reader, even though the content is visible. The audit check is mechanical: for every ARIA role that appears, look up the official required list from the WAI-ARIA Authoring Practices and match it against the actual markup, including whether the state updates reactively.

5. Role misuse: div and span instead of semantic elements

Role misuse happens when a generic element like <div> or <span> is turned into an interactive role via ARIA without rebuilding native keyboard operability. <div role="button">Save</div> is by definition focusable via tabindex="0", but without that addition, without a keydown handler for Enter and Space, and without a visible focus ring, it remains unreachable for keyboard users. A <button> gets all of that for free from the browser, whereas a <div role="button"> requires at least four additional implementation steps that in practice almost never get fully implemented.

A second, subtler pattern is the wrong role hierarchy: role="list" on an element whose direct children lack role="listitem" breaks the semantic tree structure that some screen readers need to announce "list with 5 items". Once a CSS reset with list-style: none affects native list semantics in some browsers, this problem gets worse still, which is why unstyled lists should explicitly add role="list" on the <ul> without touching the <li> elements.

6. Live regions: using aria-live correctly

Live regions announce dynamic content changes without forcing the user to lose focus, for example form errors, cart updates, or loading indicators. The most common ARIA mistake with live regions is choosing the wrong urgency level: aria-live="assertive" immediately interrupts any speech the screen reader is currently producing and should be reserved exclusively for time-critical information such as error messages. For less urgent updates like "item added to cart", aria-live="polite" is the right choice, because the announcement waits until the current speech output finishes.

A second mistake concerns timing: the container carrying aria-live must already exist in the DOM at initial render so the screen reader recognizes it as a live region. If the entire element, including the aria-live attribute, is only inserted later via JavaScript, the first update is often missed because the screen reader has not registered the region yet. The robust solution is a permanently present, initially empty live-region container in the initial markup, whose text content is later updated via JavaScript, instead of inserting the whole element along with the attribute only when needed.

A third, often overlooked mistake is forgetting aria-atomic="true" when only part of a live region gets updated. Without this attribute, some screen readers only read out the changed substring, for example a single number in "3 items in cart", instead of the full, understandable sentence. Especially with cart counters or price displays that change frequently through Alpine.js reactivity, this leads to fragmented, incomprehensible announcements for screen reader users.

7. Labeling: aria-label, aria-labelledby, and aria-describedby

The three labeling attributes are frequently confused. aria-label provides an invisible, standalone name for an element and completely overrides any visible text, even for sighted users relying on voice control software such as Dragon, which depends on the visible text to click elements by voice command. aria-labelledby instead references the ID of an already existing visible element, and is therefore almost always the better choice, because visible and accessible text stay consistent. aria-describedby adds supplementary description text, such as form hints or error messages, without changing the primary name.

A frequent ARIA mistake is aria-label on an icon button whose visible text changes independently, for instance through a later translation or a CMS update, so screen reader users and sighted users end up with different information. To visually hide text via CSS while keeping it available to screen readers, only the established sr-only class should be used, never display: none or visibility: hidden, because both CSS properties also remove the element from the accessibility tree.


/* Visually hidden but still available to screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* WRONG for accessible text: these remove the element from the a11y tree too */
.hidden-wrong {
  display: none;        /* also removes from accessibility tree */
  visibility: hidden;    /* also removes from accessibility tree */
}

/* Focus ring must remain visible even when custom-styled */
.sr-only.focusable:focus {
  position: static;
  width: auto;
  height: auto;
  overflow: visible;
  clip: auto;
}

8. ARIA in Hyvä components: modals, dropdowns, and tabs with Alpine.js

Hyvä themes build interactive components almost exclusively with Alpine.js, which makes ARIA reactivity considerably easier than in classic jQuery themes, because x-bind keeps state automatically in sync with the actual application state. Still, a typical ARIA mistake is common in Hyvä projects: a dropdown ships with a static aria-expanded="false" in the template, while the actual open/close state is only controlled through a Tailwind class like x-show, without aria-expanded being bound to the same Alpine variable via x-bind:aria-expanded.

A fully accessible dropdown requires at least four pieces to work together: a trigger button with aria-haspopup and a reactive aria-expanded, a panel with the appropriate role such as role="menu", an Escape key to close that returns focus to the trigger, and a focus trap while the panel is open. This exact combination is already delivered out of the box by the Hyvä-Alpine plugin @alpinejs/focus through its x-trap directive, so it should be used consistently when building custom modals and mega menus instead of writing error-prone custom focus logic.


// Alpine.js accessible dropdown component for a Hyvä template
document.addEventListener('alpine:init', () => {
  Alpine.data('accessibleDropdown', () => ({
    open: false,

    toggle() {
      this.open = !this.open;
    },

    close(returnFocus = true) {
      this.open = false;
      if (returnFocus) {
        this.$refs.trigger.focus();
      }
    },

    init() {
      // keep aria-expanded in sync with the real state, never hardcode it
      this.$watch('open', (value) => {
        this.$refs.trigger.setAttribute('aria-expanded', value);
      });
    }
  }));
});
Situation Broken ARIA Correct pattern Why it matters
Native button <button role="button"> <button> Redundant role only adds maintenance risk
Hidden region with a link aria-hidden without tabindex="-1" inert or DOM removal Prevents silent keyboard focus
Custom dropdown Static aria-expanded="false" x-bind:aria-expanded Screen reader knows the real state
Clickable region <div onclick> without a role <button> or role+tabindex+keydown Keyboard and screen reader access
Status message Text update without aria-live Permanent aria-live="polite" region Screen reader announces changes automatically

9. ARIA audit: mistakes in a before-and-after comparison

A systematic ARIA audit does not start with reading code, but with actually trying the page using a real screen reader, such as NVDA on Windows or VoiceOver on macOS, combined with keyboard-only operation without a mouse. Automated tools like axe-core or Lighthouse reliably catch structural mistakes such as missing required states or incorrect role hierarchies, but they cannot tell whether an announcement makes sense in context or whether a focus trap actually works. That is why manual testing is a mandatory part of a complete audit workflow.

In practice a two-stage approach works well: first, automated regression tests with axe-core in the CI pipeline that catch new, obvious violations before merge. Then, a manual sample check of the most important user flows, especially checkout, cart, and forms, using real keyboard and screen reader navigation. The comparison table and the sample report below show how such an audit categorizes and prioritizes typical mistakes.


{
  "tool": "axe-core",
  "url": "https://shop.example.com/checkout",
  "violations": [
    {
      "id": "aria-hidden-focus",
      "impact": "serious",
      "description": "aria-hidden element contains a focusable descendant",
      "nodes": [
        { "target": [".mobile-nav[aria-hidden='true'] a"], "failureSummary": "Fix: add tabindex=-1 or remove element from DOM while hidden" }
      ]
    },
    {
      "id": "aria-required-attr",
      "impact": "critical",
      "description": "Required ARIA attribute is missing",
      "nodes": [
        { "target": ["[role='tab']"], "failureSummary": "Fix: add aria-selected to every element with role=tab" }
      ]
    },
    {
      "id": "aria-allowed-role",
      "impact": "moderate",
      "description": "ARIA role is not allowed for this element",
      "nodes": [
        { "target": ["h2[role='presentation']"], "failureSummary": "Fix: remove role or use a non-heading element" }
      ]
    }
  ]
}

Prioritization follows the WCAG impact level: critical and serious violations such as missing required states or focusable hidden elements block core functionality completely and must be fixed before every release. moderate violations such as superfluous roles degrade the experience but do not prevent a task entirely, and can be batched into the next sprint. This prioritization keeps a report with a hundred entries from paralyzing the team instead of giving them concrete next steps.

Mironsoft

ARIA audits, accessibility refactoring, and accessible Hyvä components

Need to find and fix ARIA mistakes reliably?

We audit your Magento and Hyvä frontends with automated tools and real screen reader navigation, prioritize violations by impact, and implement the fixes directly in your Alpine.js components and templates.

ARIA audit

axe-core scans combined with manual NVDA and VoiceOver testing

Component refactoring

Dropdowns, modals, and tabs with correct states and focus traps

CI integration

axe-core regression tests firmly anchored in the deployment pipeline

10. Summary

The first rule of ARIA remains the single most important principle of any accessibility audit: use native HTML elements wherever possible, and reach for ARIA only where native semantics fall short. Redundant roles on <button> or <nav> add no value and only increase maintenance risk. aria-hidden on containers with focusable children creates silent, unreachable keyboard traps, which are avoided with inert or consistent DOM removal. Missing required states such as aria-checked or aria-selected make a role meaningless to screen readers, even when the role itself is set correctly.

Role misuse on generic <div> and <span> elements requires four extra implementation steps that native elements provide for free. Live regions must be present in the DOM from the start and use the correct urgency level. In Hyvä projects, x-bind reliably synchronizes ARIA states with the actual Alpine.js state when applied consistently. A two-stage audit workflow combining automated axe-core scans in the CI pipeline with manual screen reader testing reliably catches both structural and contextual ARIA mistakes.

Avoiding common ARIA mistakes, the essentials at a glance

First rule of ARIA

Use native elements instead of rebuilding them with role. No ARIA is better than bad ARIA.

aria-hidden with care

Never on containers with focusable children lacking tabindex="-1". Use inert for fully blocked regions.

Check required states

Every role has mandatory states like aria-checked or aria-selected. Update reactively, never set statically.

Two-stage audit

axe-core in the CI pipeline for structure, NVDA/VoiceOver manually for real user experience.

11. FAQ: Avoiding Common ARIA Mistakes

1What is the first rule of ARIA?
A native HTML element with the desired semantics should always be preferred over a generic element repurposed with ARIA. ARIA does not replace native keyboard behavior.
2Why is role="button" on a native button redundant?
The native button already exposes its role through the accessibility tree. The extra attribute adds nothing but increases the risk of conflicting state.
3Why must aria-hidden never sit on focusable elements?
It only removes elements from the accessibility tree, not from the tab order. Screen reader users land on silent elements. inert or DOM removal is the more robust fix.
4Which states does a custom dropdown absolutely need?
At minimum aria-haspopup and a reactive aria-expanded on the trigger, an appropriate role like role="menu" on the panel, plus aria-selected on options for selection lists.
5Why is a div with role="button" problematic?
The role creates expectations that go unmet without tabindex, keyboard handlers, and a focus ring. A native button element avoids this entirely.
6polite or assertive for live regions?
assertive only for time-critical errors, interrupts speech immediately. polite waits and suits status messages like cart updates.
7aria-label vs. aria-labelledby vs. aria-describedby?
aria-label gives a standalone invisible name, aria-labelledby references visible text, aria-describedby adds description without changing the name.
8Accessible dropdown with Alpine.js in Hyvä?
Bind aria-expanded via x-bind to the Alpine state, implement Escape to close with focus returning to the trigger, and use x-trap from @alpinejs/focus while the panel is open.
9Which tools find ARIA mistakes automatically?
axe-core and Lighthouse reliably surface structural mistakes. Contextual issues like meaningful announcements need additional manual screen reader testing.
10Can ARIA make a page less accessible?
Yes, incorrectly applied ARIA can override native semantics or report the wrong state. That is why no ARIA is better than badly implemented ARIA.