webdev.complete
โ™ฟ Accessibility Deep
๐ŸšฆGoing to Production
Lesson 104 of 117
25 min

WCAG & ARIA

Semantic first. ARIA roles, states, live regions.

A surprisingly large fraction of the web is unusable by a surprisingly large fraction of its users, and the reason is almost always avoidable. Accessibility is not a special-needs accommodation; it's the baseline assumption that your interface will be operated by a person you cannot see, on a device you didn't choose, in conditions you can't imagine. WCAG and ARIA are the two vocabularies you need to know.

WCAG in one paragraph: POUR

The Web Content Accessibility Guidelines (currently version 2.2) are organized around four principles, abbreviated POUR:

  • Perceivable. Content can be perceived by at least one sense. Images have alt text, videos have captions, color isn't the only signal.
  • Operable. Everything that can be done with a mouse can be done with a keyboard. Nothing flashes faster than 3 times a second. There are no time limits you can't extend.
  • Understandable. Pages are predictable. Form errors say what to fix in plain language.
  • Robust. The HTML is valid enough that assistive tech can parse it. ARIA is used correctly (or not at all).

Conformance levels: A, AA, AAA

Each WCAG success criterion is rated A (must), AA (should), or AAA (nice). The industry baseline most laws (ADA, EAA, Section 508) reference is AA. Examples:

  • A"Non-text content has a text alternative." (Alt text on images.)
  • AA"Text has a contrast ratio of at least 4.5:1." (Large text: 3:1.)
  • AAA"Sign language is provided for prerecorded audio."
Target AA
AAA is admirable but often unreasonable for consumer products (it forbids justified text, for instance). Hit AA across the board first.

Color contrast, the most-failed rule

Roughly half of all WCAG failures are contrast. Memorize the AA numbers:

  • 4.5:1 for normal body text.
  • 3:1for "large text" (24px+, or 18.66px+ bold) and UI components and graphical objects.
css
/* Fails AA - 2.85:1 on white */
.muted { color: #999; }

/* Passes AA - 4.54:1 on white */
.muted-ok { color: #767676; }

/* Passes AA at 4.54:1, but only for "large" text */
.heading-muted { color: #949494; font-size: 24px; }
Disabled is not exempt
A common myth: "disabled controls don't need to meet contrast." True under the letter of WCAG, false in spirit. If a disabled button is unreadable, users can't tell what action is unavailable. Aim for at least 3:1 anyway.

ARIA: roles, states, properties

Accessible Rich Internet Applications (ARIA) is a vocabulary you add to HTML to describe interface widgets that HTML doesn't natively cover. Three categories:

  • Role - what is this thing? role="tab", role="dialog", role="alert".
  • State - what condition is it in? aria-expanded, aria-checked, aria-selected.
  • Property - additional info that rarely changes. aria-label, aria-describedby, aria-controls.

The first rule of ARIA

No ARIA is better than wrong ARIA
The most common ARIA bug is misuse. A wrong role lies to screen readers. If a native HTML element already does the job, use it. <button> beats <div role="button" tabIndex=0> every single time.

Concretely:

html
<!-- Bad: reinventing the wheel, badly -->
<div role="button" tabindex="0" onclick="save()">Save</div>

<!-- Good: native semantics, focus, keyboard, all free -->
<button onclick="save()">Save</button>

When ARIA does help: building a tablist

HTML has no <tab> element. So you build one and decorate it with ARIA so assistive tech understands the relationships:

html
<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true"  aria-controls="panel-1" id="tab-1">Profile</button>
  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">Billing</button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  <!-- profile content -->
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  <!-- billing content -->
</div>

Roles announce structure. aria-selected reflects state. aria-controlswires tab to panel. Plus you handle arrow keys in JS, because that's the expected tab interaction pattern.

aria-label vs aria-labelledby vs aria-describedby

  • aria-label="Close" - provides a name when none is visible. Use on icon buttons.
  • aria-labelledby="heading-id" - name comes from another element. Use to pair a dialog with its visible title.
  • aria-describedby="hint-id" - extra description read after the name. Use for help text under form fields.
html
<!-- Icon button: no text, so we add a label -->
<button aria-label="Close dialog">
  <svg aria-hidden="true">...</svg>
</button>

<!-- Dialog: name comes from its heading -->
<div role="dialog" aria-modal="true" aria-labelledby="dlg-title">
  <h2 id="dlg-title">Delete project?</h2>
  ...
</div>

<!-- Form field with extra help -->
<label for="pw">Password</label>
<input id="pw" type="password" aria-describedby="pw-help">
<p id="pw-help">Must be at least 12 characters.</p>

Live regions: announcing what changed

When content updates without a page reload (a toast appears, a search result count changes), sighted users see it. Screen reader users don't - unless you tell the screen reader by marking a region as live.

html
<!-- Polite: announces when the screen reader is idle -->
<div aria-live="polite" id="search-status">
  3 results found
</div>

<!-- Assertive: interrupts. Use sparingly - errors, urgent alerts. -->
<div role="alert">
  Could not save. Check your connection.
</div>

Rules of thumb: put the live region in the DOM at page load (empty is fine). Change its text content to trigger an announcement. Use aria-live="polite" by default. Save role="alert" (which implies aria-live="assertive") for things that genuinely need to interrupt.

Test what you write
ARIA is invisible by default. The only way to know if it works is to turn on a screen reader (VoiceOver on macOS is free, NVDA on Windows is free) and listen. Five minutes of testing reveals more than five hours of reading docs.

Quiz

Quiz1 / 4

Which WCAG conformance level is the typical legal and industry baseline?

Recap

  • WCAG 2.2 organized as POUR: Perceivable, Operable, Understandable, Robust. Aim for AA.
  • Contrast: 4.5:1 body text, 3:1 large text and UI.
  • ARIA = roles + states + properties. No ARIA is better than wrong ARIA.
  • aria-label names hidden things, aria-labelledby pairs with another element, aria-describedby adds extra description.
  • Use aria-live="polite" to announce DOM updates without interrupting.
  • Test with a real screen reader. ARIA is invisible until you listen.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.