Web Accessibility Basics: ARIA Without the Guesswork
The most common ARIA mistake isn't using the wrong attribute — it's reaching for ARIA at all when a native HTML element would have solved the problem for free. ARIA (Accessible Rich Internet Applications) exists to describe custom widgets that HTML has no built-in element for, not to patch up a <div> that's standing in for a <button>. The first rule of ARIA, straight from the spec authors, is: don't use ARIA if you don't have to.
The div-button problem
This shows up constantly in component libraries:
<div class="button" onclick="submitForm()">Submit</div>
Visually it's a button. To a screen reader, it's an unlabeled, non-interactive chunk of text — not focusable with Tab, not announced as a button, not activated by Enter or Space. You can patch all of that back in with ARIA and JavaScript:
<div
class="button"
role="button"
tabindex="0"
onclick="submitForm()"
onkeydown="if (event.key === 'Enter' || event.key === ' ') submitForm()"
>
Submit
</div>
Or you can use the element that already does all of this:
<button onclick="submitForm()">Submit</button>
Keyboard support, focus handling, the accessible name, the correct role — all free, all consistent across every browser and screen reader, zero JavaScript required. This is the pattern to internalize: reach for <button>, <a href>, <input>, <select>, and friends first, and only step outside HTML's native semantics when you're building something HTML genuinely has no element for, like a tab panel or a combobox with autocomplete.
When ARIA earns its place: labeling
Even with native elements, some things need extra context that visible text doesn't convey. An icon-only button is the classic case:
<button aria-label="Close dialog">
<svg><!-- X icon --></svg>
</button>
Without aria-label, a screen reader announces this as just "button" — technically focusable and activatable, but the user has no idea what it does. aria-label supplies an accessible name when there's no visible text to serve as one. If there is visible text nearby but it's not inside the button itself, aria-labelledby points to it by ID instead of duplicating it:
<h2 id="billing-heading">Billing Address</h2>
<button aria-labelledby="billing-heading">Edit</button>
This announces as "Edit, Billing Address" — more useful than a bare "Edit" when a page has five identical edit buttons for five different sections.
Describing state on custom widgets
This is where ARIA is doing real, load-bearing work: custom components need to announce state changes that native elements handle automatically. A collapsible section:
<button aria-expanded="false" aria-controls="panel-1" id="toggle-1">
Show details
</button>
<div id="panel-1" hidden>
...
</div>
const toggle = document.getElementById('toggle-1');
const panel = document.getElementById('panel-1');
toggle.addEventListener('click', () => {
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!isExpanded));
panel.hidden = isExpanded;
});
aria-expanded has to be kept in sync with the actual visible state in JavaScript — ARIA attributes never do anything on their own, they're purely informational for assistive technology. Set aria-expanded="false" and never update it, and a screen reader user will be told the panel is collapsed forever, even after they've opened it.
Live regions for dynamic updates
When content changes without a page navigation or a focus change — a form validation error appearing, a "saved" toast, search results updating — sighted users see it happen, but a screen reader has no reason to announce something outside the user's current focus unless you tell it to.
<div aria-live="polite" id="save-status"></div>
document.getElementById('save-status').textContent = 'Changes saved.';
aria-live="polite" tells assistive technology to announce the change once the user pauses whatever they're currently doing — appropriate for status messages. Use aria-live="assertive" only for things that genuinely need to interrupt, like a session-expiry warning; overusing it makes a page exhausting to use with a screen reader, since every minor update cuts off whatever was just being read.
The rule that catches the most bugs
If you only take one habit from this: after adding any role or aria-* attribute, ask what native element could have avoided the need for it. role="button" on a <div> → should've been <button>. role="navigation" on a generic wrapper → should've been <nav>. aria-required="true" on a text input → should've been the required attribute, which conveys the same thing to assistive tech and also gets you free browser-level validation.
Then, for the ARIA you do keep: run it through a screen reader once. VoiceOver ships with every Mac (Cmd+F5 to toggle) and NVDA is free on Windows. Five minutes of navigating your own interface by keyboard and ear catches more real problems than reading the ARIA spec cover to cover — you'll immediately notice an unlabeled icon button or a live region that never fires, because you'll be the one confused by the silence.
Takeaway
ARIA supplements HTML's semantics for the cases HTML doesn't cover — it doesn't replace using the right element in the first place. Start with native HTML, add aria-label/aria-labelledby where the accessible name isn't obvious from visible text, use state attributes like aria-expanded only when you also keep them in sync with reality, and reserve live regions for updates a user genuinely needs to hear about outside their current focus.