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."
Which version, and by when
WCAG 2.2 has been the W3C Recommendation since October 2023 and is what you should be building against. WCAG 3.0 exists, but it is still a Working Draft that reorganises the whole model, and the W3C itself says it is years from being a standard. Nothing you ship this year needs to care about it.
Regulation lags the spec. The US Department of Justice rule for state and local government sites names WCAG 2.1 Level AA, not 2.2, and its deadlines were pushed back in April 2026: entities serving populations of 50,000 or more now have until 26 April 2027, and smaller entities and special districts until 26 April 2028. The European standard, EN 301 549, also still references WCAG 2.1. Building to 2.2 AA covers both, since 2.2 only adds criteria.
On the ARIA side, ARIA 1.2 is the current Recommendation. ARIA 1.3 is in draft and adds things like aria-description and braille attributes, so treat anything from it as not-yet-safe.
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:1 for "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-controls wires 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.