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."
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.
/* 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; }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
<button> beats <div role="button" tabIndex=0> every single time.Concretely:
<!-- 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:
<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.
<!-- 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.
<!-- 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.
Quiz
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-labelnames hidden things,aria-labelledbypairs with another element,aria-describedbyadds extra description.- Use
aria-live="polite"to announce DOM updates without interrupting. - Test with a real screen reader. ARIA is invisible until you listen.