Accessibility & ARIA
Web accessibility (a11y) ensures websites are usable by people with disabilities. HTML is the foundation — ARIA fills the gaps when native semantics aren't enough.
🌍 Who benefits?
~1.3 billion people worldwide have a disability. Includes visual, hearing, motor, cognitive impairments — plus temporary impairments like a broken arm or bright sunlight.
⚖️ Legal requirement
WCAG 2.1 AA is legally required for government and public sector websites in many countries (ADA, EN 301 549, Accessibility Act). Failure can lead to lawsuits.
📈 SEO benefit
Search engines are essentially blind users — they crawl text. Good semantic HTML and accessible descriptions directly improve search rankings.
🎯 Everyone benefits
Captions help non-native speakers. High contrast helps in bright sunlight. Keyboard navigation helps power users. Good a11y = good UX.
WCAG 4 Core Principles (POUR)
| Principle | Meaning | HTML Techniques |
|---|---|---|
| Perceivable | Information must be presentable to all users in ways they can perceive. | alt text on images, captions on video, sufficient color contrast, don't convey info by color alone. |
| Operable | UI components must be operable. | Full keyboard navigation, visible focus indicators, no keyboard traps, no seizure-inducing content, skip navigation links. |
| Understandable | Information and operation must be understandable. | Set lang attribute, clear error messages, consistent navigation, predictable behavior. |
| Robust | Content must be robust enough to be interpreted by assistive technologies. | Valid semantic HTML, ARIA attributes, proper DOM structure. |
Rule #1: Native HTML First
Native HTML elements have built-in accessibility. Always prefer them over custom implementations + ARIA.
✅ DO — Use native elements
<button>Submit</button><input type="checkbox"><nav>...</nav><label for="name">Name</label>
❌ DON'T — Recreate with divs + ARIA
<div role="button" tabindex="0">Submit</div><div role="checkbox" tabindex="0"><div role="navigation"><div id="name-label">Name</div>
ARIA Landmark Roles
Landmark roles create navigation landmarks that screen reader users can jump between. Most map to native HTML5 elements:
| ARIA Role | Native Equivalent | Purpose |
|---|---|---|
| role="banner" | <header> (at page level) | Site-wide header. Only one per page. |
| role="navigation" | <nav> | A collection of navigation links. |
| role="main" | <main> | The primary content of the document. Only one per page. |
| role="complementary" | <aside> | Supporting content related to the main content. |
| role="contentinfo" | <footer> (at page level) | Information about the page (copyright, privacy policy, contact). One per page. |
| role="search" | <search> | A search landmark. |
| role="form" | <form> (with aria-label) | A form landmark. A <form> only creates a landmark if it has an accessible name. |
| role="region" | <section aria-label="..."> | A perceivable landmark. Must have an accessible name (aria-label or aria-labelledby). |
Essential ARIA States & Properties
| Attribute | Values | Use Case |
|---|---|---|
| aria-label | String | Accessible name for elements with no visible label. E.g., icon-only buttons: <button aria-label="Close"><i class="fa fa-x"></i></button> |
| aria-labelledby | ID(s) | Points to one or more elements whose text content is the accessible name. Overrides aria-label. |
| aria-describedby | ID(s) | Points to elements providing additional description. E.g., password requirements, error messages. |
| aria-hidden | true/false | true: removes from accessibility tree (decorative icons). false: includes hidden elements. |
| aria-live | off/polite/assertive | Dynamic content regions. polite: announces after current speech. assertive: interrupts immediately. |
| aria-atomic | true/false | With aria-live: true = announce entire region when any part changes. |
| aria-relevant | additions/removals/text/all | What type of live region changes to announce. |
| aria-expanded | true/false/undefined | Whether a collapsible element (accordion, dropdown) is open or closed. |
| aria-selected | true/false/undefined | Whether an item in a listbox, tab list, or tree is selected. |
| aria-checked | true/false/mixed | Checked state of a custom checkbox or radio. mixed = indeterminate. |
| aria-pressed | true/false/mixed | Pressed state of a toggle button. |
| aria-disabled | true/false | Marks element as disabled in accessibility tree without removing from tab order. |
| aria-invalid | true/false/grammar/spelling | Marks a form field as having an invalid value. Use with aria-describedby pointing to the error message. |
| aria-required | true/false | Indicates required field when native required attribute can't be used. |
| aria-current | page/step/location/date/time/true | Indicates the current item within a set. Use aria-current="page" for active nav links. |
| aria-busy | true/false | Indicates a region is loading. Screen readers wait before announcing content. |
| aria-modal | true/false | On dialog/modal elements — tells screen readers the background is inert. |
| aria-haspopup | true/menu/listbox/tree/grid/dialog | Indicates the element can open a popup of the specified type. |
| aria-owns | ID(s) | Defines a parent-child relationship when DOM structure doesn't match visual structure. |
| aria-controls | ID | Identifies the element(s) this element controls. |
| aria-posinset | Integer | Position of the element within a set. Used with aria-setsize. |
| aria-setsize | Integer | Total number of items in the set. |
| aria-level | Integer (1-6) | Hierarchical level for tree items or headings when native headings aren't used. |
| aria-placeholder | String | Short hint for a custom text field (mirrors native placeholder). |
| aria-valuemin/max/now | Number | For sliders, spinners, and progress bars — minimum, maximum, and current values. |
| aria-valuetext | String | Human-readable text for the current value. Use instead of aria-valuenow when number is not meaningful alone (e.g., "Monday" for a day picker). |
Skip Navigation Link Pattern
Chapter Summary
- Always prefer native semantic HTML over ARIA overrides — ARIA is a last resort
- Every image needs
alt; every video needs<track>for captions - Add
aria-labelto icon-only buttons so screen readers can identify them - Use
aria-live="polite"for dynamic content updates (loading states, notifications) - Add a "Skip to main content" link as the first element for keyboard navigation