C.W.K.
Stream
Lesson 03 of 04 · published

ARIA Baseline: The Practical Subset You'll Actually Use

~12 min · aria, screen-reader, labels, live-regions

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The First Rule of ARIA is: don't use ARIA. The Second Rule is: when you have to, use this small set well."

The Spec Itself Tells You To Avoid It

The W3C's Using ARIA document opens with a literal warning: "If you can use a native HTML element... 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." Most ARIA misuse comes from adding ARIA to elements that already had the right semantics. <button aria-label="Submit">Submit</button> — the aria-label is silently redundant; the button's text content is already the accessible name.

So when does ARIA earn its keep? Five situations cover 90% of legitimate cases.

1. Naming An Unnamed Element

When the visible text isn't a useful name — icon-only buttons, search inputs labeled only by a magnifying-glass icon, close buttons that show only an X — give them a name.

  • aria-label="Close menu" — the accessible name comes from this attribute (use when no visible label exists).
  • aria-labelledby="id-of-another-element" — the name comes from text inside another element (use when the label is visible somewhere else on screen).

2. Describing An Element

For helper text, hints, or validation messages associated with an input:

  • aria-describedby="id" — points to the element holding the description. Screen readers read the name first, then the description. Use for password hints ("At least 8 characters with one number"), validation errors, format guidance.

3. Communicating State

When an interactive element's state matters but isn't obvious from its native semantics:

  • aria-expanded="true" | "false" — for buttons that open menus, accordions, dropdowns.
  • aria-pressed="true" | "false" — for toggle buttons (think bold/italic in an editor).
  • aria-checked="true" | "false" | "mixed" — for custom checkboxes (mixed = indeterminate).
  • aria-selected="true" | "false" — for items in a custom tab list or listbox.
  • aria-disabled="true" — for elements that look disabled but you still want focusable (so screen reader users know it exists). Different from the disabled attribute.

4. Form Validation

  • aria-required="true" — only needed when you can't use the native required attribute (custom form controls). With native HTML inputs, required already sets this.
  • aria-invalid="true" — paired with the field once validation fails. Screen readers announce "invalid entry" when the field gets focus. Pair with aria-describedby pointing at the error message.

5. Live Regions For Dynamic Content

When content updates without the user navigating (a toast notification, a save confirmation, search results that filter as the user types), screen readers won't notice — unless the updated region is a live region:

  • aria-live="polite" — announces the update when the screen reader is idle. Default choice for most notifications.
  • aria-live="assertive" — interrupts whatever the screen reader is saying. For critical messages (errors, lost connection). Use sparingly — it's the all-caps shout.
  • role="status" — shorthand for polite live region. Use for status messages.
  • role="alert" — shorthand for assertive live region. Use for errors.

Hiding From Assistive Tech: aria-hidden="true"

When something is visible but should be skipped by screen readers — decorative icons, redundant text — set aria-hidden="true". Most common case: a button that has both a visible icon and a visible text label. The icon is decorative; the text is the name. Hide the icon from screen readers so it doesn't announce "image, settings" before "button, Settings."

Never put aria-hidden="true" on a focusable element. If a screen-reader user can tab to an element that's hidden from them, they hear nothing when they arrive. Confusing at best, broken at worst. If you need to hide something AND make it unfocusable, also add tabindex="-1" or the native hidden attribute.

The Mantra: Native First, ARIA Second

Every line of ARIA you write is a line you have to keep correct. Native HTML elements give you all the right semantics for free and the browser updates them automatically. ARIA you write by hand has to be kept in sync as state changes — every aria-expanded, every aria-checked, every aria-invalid requires JavaScript to maintain. Native wins on maintenance every time.

Pippa's Note

The cwkPippa chat input has just three ARIA properties in production: aria-label="Message Pippa" on the textarea (because the visible label is hidden for visual design), aria-busy="true" when she's streaming a reply (so screen readers know to wait), and aria-live="polite" on the new-message announcement region (so a screen reader user is told when her reply lands). That's it. Everything else is native — <button>, <label>, <a>, semantic roles by markup choice. Small ARIA surface, fewer places for state to drift out of sync.

Code

Names and descriptions, the legitimate ARIA·html
<!-- ARIA earning its keep: icon-only buttons -->
<button aria-label="Close menu">
  <svg aria-hidden="true" width="24" height="24">...</svg>
</button>

<button aria-label="Send message" type="submit">
  <svg aria-hidden="true">...</svg>
</button>

<!-- aria-labelledby — name from another visible element -->
<h2 id="section-prefs">Preferences</h2>
<section aria-labelledby="section-prefs">
  <p>Settings for theme, voice, and notifications.</p>
</section>

<!-- aria-describedby for help text -->
<label for="new-pw">New password</label>
<input id="new-pw" type="password"
       aria-describedby="pw-rules" required minlength="8" />
<small id="pw-rules">At least 8 characters, one number.</small>
aria-expanded — state that must stay in sync·html
<!-- Expand/collapse state on a disclosure button -->
<button aria-expanded="false"
        aria-controls="settings-panel"
        id="settings-toggle">
  Settings
</button>
<section id="settings-panel" hidden>
  ...
</section>

<script>
  const btn = document.getElementById('settings-toggle');
  const panel = document.getElementById('settings-panel');
  btn.addEventListener('click', () => {
    const isOpen = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', String(!isOpen));
    panel.hidden = isOpen;
  });
</script>
Live regions: polite vs assertive·html
<!-- Live regions for dynamic updates -->
<form>
  <label for="email-x">Email</label>
  <input id="email-x" type="email" aria-describedby="email-error" />
  <p id="email-error" role="alert"></p>  <!-- assertive: errors interrupt -->
</form>

<!-- Toast notifications: polite -->
<div role="status" aria-live="polite" id="toast"></div>

<script>
  function showToast(message) {
    document.getElementById('toast').textContent = message;
  }
  // showToast('Settings saved.') — screen readers will announce it.
</script>

External links

Exercise

Take an icon-only button you've seen (close, settings, search) and write three versions: (1) with aria-label, (2) with aria-labelledby pointing to a visually-hidden span, (3) with visible text rendered with CSS. Open each in Chrome DevTools → Accessibility tab and confirm the screen reader name is correct. Bonus: build a tiny toast component with aria-live="polite" — does VoiceOver announce it on update?
Hint
If the screen reader announces nothing, the live region might be empty when the page loads (some browsers ignore live regions that didn't exist at announcement time). Render the container empty in the initial HTML, then update its text via JavaScript.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.