ARIA fills the gaps HTML can’t — learn roles, properties, and states, when an aria-label helps, how to announce dynamic updates with live regions, and the first rule of ARIA: don’t use it if HTML already does the job.
Why: ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that tell assistive tech what an element is and what state it is in. But the first rule of ARIA is: don't use ARIA if a native element already does it. A real <button> beats <div role="button"> every time. Note: wrong or excess ARIA is worse than none — it actively lies to screen readers.
<!-- ❌ Re-implementing a button with ARIA + JS you now have to maintain -->
<div role="button" tabindex="0" onclick="…">Submit</div>
<!-- ✅ The native element already has the role, focus, and keyboard behavior -->
<button>Submit</button>Why: ARIA has three kinds of attribute. A role says what something is (role="dialog", role="tab", role="alert", role="region"). A property describes it and rarely changes (aria-label). A state describes a condition that does change (aria-expanded, aria-checked). Note: reach for these only on custom widgets HTML has no element for — like a tab panel or a tree.
<!-- A custom tab: role says what it is, state says it's the active one -->
<button role="tab" aria-selected="true">Overview</button>
<button role="tab" aria-selected="false">Billing</button>
<!-- A disclosure: aria-expanded flips as the panel opens and closes -->
<button aria-expanded="false" aria-controls="faq1">What's your refund policy?</button>
<div id="faq1" hidden>…</div>Why: every control needs an accessible name — the text a screen reader announces. Visible text is the best name. When there is none (an icon-only button), aria-label supplies one; when the name is other visible text on the page, aria-labelledby points to it; aria-describedby adds extra detail like a hint. Note: a bare icon button with no name is announced as just "button" — useless.
<!-- Icon-only button: no visible text, so name it with aria-label -->
<button aria-label="Close dialog">✕</button>
<!-- Name comes from other visible text via its id -->
<h2 id="billing">Billing</h2>
<section aria-labelledby="billing">…</section>
<!-- Extra hint announced after the field's label -->
<input aria-describedby="pw-hint" type="password" />
<p id="pw-hint">At least 12 characters.</p>Why: sometimes content is visual-only and would be noise if read aloud — a decorative icon next to text that already says the same thing. aria-hidden="true" removes an element from the accessibility tree while leaving it on screen. Note: never put aria-hidden on something focusable or interactive — you'd create a control keyboard users can reach but screen-reader users can't perceive.
<!-- The word "Settings" is already read; hide the redundant icon -->
<button>
<svg aria-hidden="true">…</svg>
Settings
</button>Why: when content changes without a page reload — a "Saved!" toast, a search-results count, a form error — a screen reader won't notice unless you mark the area as a live region. aria-live="polite" announces the change at the next pause; aria-live="assertive" interrupts immediately (use sparingly, for true urgency). Note: the live region must already be in the DOM before you update its text.
<!-- This empty region exists on load; updating its text gets announced -->
<div aria-live="polite" id="status"></div>
<script>
// Later, after saving:
document.querySelector('#status').textContent = 'Changes saved'
// A screen reader announces "Changes saved" without moving focus.
</script>