| name | apply-aria-roles |
| description | Use when building custom UI components that native HTML cannot represent — to give assistive technology the correct role, state, and property information for non-native interactive elements. |
| source | W3C WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A); WAI-ARIA 1.2 specification; WAI-ARIA Authoring Practices 1.2; MDN ARIA guide |
| tags | ["accessibility","wcag","a11y","aria","roles","states","properties","screen-reader"] |
| related | ["apply-keyboard-accessibility","write-semantic-html-structure","design-accessibility-standards"] |
Implement ARIA Roles
Give custom UI components a valid ARIA role, accessible name, and required states — so screen readers understand and announce them correctly.
Why This Is Best Practice
Adopted by: WCAG 2.1 SC 4.1.2 (Level A) is the robustness principle — required by
all accessibility laws. WAI-ARIA 1.2 is the W3C specification implemented in all modern
screen readers (NVDA, JAWS, VoiceOver, TalkBack). React Aria (Adobe), Headless UI
(Tailwind), Radix UI, and Angular CDK all implement WAI-ARIA patterns — used in millions
of production applications.
Impact: Deque Systems research found custom widgets without ARIA account for 38% of
critical accessibility defects in enterprise applications. A <div> with onclick and
no ARIA is announced as nothing (or as the text content) — users cannot determine the
element is interactive. ARIA provides the "what is it" signal that screen readers need
to announce controls correctly.
Why best: The alternative — styling <div> elements as buttons and hoping screen
readers infer their purpose — is unreliable. ARIA provides an explicit contract:
"this element is a tab", "this element is expanded", "this element is required".
Sources: W3C WCAG 2.1 SC 4.1.2 (2018); WAI-ARIA 1.2 (2023); WAI-ARIA Authoring
Practices 1.2; Deque Systems 2024 State of Digital Accessibility
Steps
Step 1: Follow the First Rule of ARIA — use native HTML first
<div role="button" tabindex="0" onclick="submit()">Submit</div>
<button type="submit">Submit</button>
WAI-ARIA First Rule: "If you can use a native HTML element or attribute with the
semantics and behavior you require already built in, instead of re-purposing an element
and adding an ARIA role, state or property to make it accessible, then do so."
Step 2: Match role to the component pattern exactly
Use WAI-ARIA Authoring Practices (w3.org/WAI/ARIA/apg) as the source of truth.
Common patterns:
<div role="tablist" aria-label="Account settings">
<button role="tab" aria-selected="true" aria-controls="panel-profile" id="tab-profile">Profile</button>
<button role="tab" aria-selected="false" aria-controls="panel-security" id="tab-security">Security</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">...</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>...</div>
<h3>
<button aria-expanded="true" =>Section 1
...
Delete account
This action cannot be undone.
Cancel
Delete
Step 3: Provide accessible names for all interactive elements
Every interactive element needs an accessible name via one of these methods (in priority order):
aria-labelledby — references visible text in the DOM
<label> — for form inputs
aria-label — for elements with no visible text label
title — last resort, not reliably announced by all AT
<button aria-label="Delete item">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
<section aria-labelledby="search-heading">
<h2 id="search-heading">Search results</h2>
...
</section>
Step 4: Update ARIA states when UI changes
States must be kept in sync with the UI. Stale states confuse screen readers.
function toggleAccordion(button, panel) {
const expanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
}
function startLoading(button) {
button.setAttribute('aria-busy', 'true');
button.setAttribute('aria-label', 'Saving…');
}
function stopLoading(button) {
button.removeAttribute('aria-busy');
button.setAttribute('aria-label', 'Save');
}
Common states to keep in sync: aria-expanded, aria-selected, aria-checked,
aria-disabled, aria-invalid, aria-busy.
Step 5: Validate ARIA against the specification — avoid invalid combinations
<div role="button" aria-required="true">Submit</div>
<a href="/home" role="presentation">Home</a>
<button role="link">Click</button>
Use the axe-core browser extension to validate ARIA — it flags invalid role/attribute
combinations that are invisible to manual testing.
When NOT to Use
- When a native element exists — never use
role="button" when <button> is available. Never use role="checkbox" when <input type="checkbox"> is available.
aria-hidden on focusable elements — never hide a focusable element with aria-hidden="true". It removes the element from the AT tree while leaving it in the Tab order, creating a "ghost" interactive element.
Common Mistakes
ARIA without keyboard support. role="button" makes an element look like a button to screen readers — but without tabindex="0" and keyboard event handlers, it's not keyboard operable. Role + keyboard support are inseparable.
aria-label on non-interactive elements. aria-label on a <div> with no role has no effect. ARIA attributes only apply to elements with an explicit or implicit ARIA role.
Not hiding decorative SVGs from AT. An inline SVG without aria-hidden="true" and focusable="false" will be announced by some screen readers. Decorative SVGs must be hidden: <svg aria-hidden="true" focusable="false">.