WCAG & ARIA
Interactive Accessibility Audit
Explore common accessibility issues and learn how to identify and fix them.
The Web Content Accessibility Guidelines (WCAG) and Accessible Rich Internet Applications (ARIA) are the two pillars of web accessibility. WCAG provides the principles and success criteria, while ARIA provides the technical attributes to make dynamic web content accessible. This page covers both in depth.
WCAG 2.1 Principles — POUR
WCAG is organized around four principles, known by the acronym POUR:
P — Perceivable Can users perceive the content? (text alternatives, captions, sufficient contrast)
O — Operable Can users operate the interface? (keyboard accessible, no seizure triggers, navigable)
U — Understandable Can users understand the content and interface? (readable, predictable, input assistance)
R — Robust Can the content be interpreted by assistive technologies? (valid HTML, compatible with current and future tools)Conformance Levels
| Level | Meaning | Requirement |
|---|---|---|
| A | Minimum accessibility | Must be met (baseline) |
| AA | Addresses major barriers | Target for most organizations (legal standard in many countries) |
| AAA | Highest accessibility | Aspirational (not usually required for entire sites) |
Key WCAG Success Criteria
Perceivable
| Criterion | Level | Description |
|---|---|---|
| 1.1.1 Non-text Content | A | All non-text content has a text alternative (alt text, labels) |
| 1.2.1 Audio/Video (Prerecorded) | A | Provide captions and audio descriptions |
| 1.3.1 Info and Relationships | A | Structure conveyed through markup, not just visual formatting |
| 1.3.4 Orientation | AA | Content works in both portrait and landscape orientations |
| 1.4.1 Use of Color | A | Color is not the only means of conveying information |
| 1.4.3 Contrast (Minimum) | AA | 4.5:1 for normal text, 3:1 for large text |
| 1.4.4 Resize Text | AA | Text can be resized up to 200 percent without loss of functionality |
| 1.4.11 Non-text Contrast | AA | UI components and graphics have 3:1 contrast ratio |
Operable
| Criterion | Level | Description |
|---|---|---|
| 2.1.1 Keyboard | A | All functionality is available from a keyboard |
| 2.1.2 No Keyboard Trap | A | Keyboard focus can move away from any component |
| 2.2.1 Timing Adjustable | A | Users can extend, adjust, or disable time limits |
| 2.3.1 Three Flashes | A | No content flashes more than 3 times per second |
| 2.4.1 Bypass Blocks | A | Skip navigation mechanism provided |
| 2.4.3 Focus Order | A | Focus order preserves meaning and operability |
| 2.4.6 Headings and Labels | AA | Headings and labels describe topic or purpose |
| 2.4.7 Focus Visible | AA | Keyboard focus indicator is visible |
| 2.5.1 Pointer Gestures | A | Multipoint gestures have single-pointer alternatives |
Understandable
| Criterion | Level | Description |
|---|---|---|
| 3.1.1 Language of Page | A | Default language is programmatically set |
| 3.1.2 Language of Parts | AA | Language of each passage is programmatically set |
| 3.2.1 On Focus | A | No unexpected context changes on focus |
| 3.2.2 On Input | A | No unexpected context changes on input |
| 3.3.1 Error Identification | A | Errors are identified and described to the user |
| 3.3.2 Labels or Instructions | A | Input fields have labels or instructions |
| 3.3.3 Error Suggestion | AA | Error messages suggest corrections |
Robust
| Criterion | Level | Description |
|---|---|---|
| 4.1.1 Parsing | A | HTML is valid and properly nested |
| 4.1.2 Name, Role, Value | A | All UI components have accessible names and roles |
| 4.1.3 Status Messages | AA | Status messages announced without focus change |
ARIA — Accessible Rich Internet Applications
ARIA provides attributes that supplement HTML to make dynamic web content accessible. It defines roles, states, and properties.
The Five Rules of ARIA
ARIA Roles
Roles define the type of UI element:
<!-- Widget roles --><div role="button">Click me</div><div role="checkbox" aria-checked="false">Option</div><div role="tab">Tab 1</div><div role="dialog" aria-modal="true">Modal content</div><div role="alert">Error message</div><div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"></div><div role="slider" aria-valuenow="50"></div><div role="switch" aria-checked="true">Dark mode</div>
<!-- Document structure roles --><div role="heading" aria-level="2">Section Title</div><div role="list"><div role="listitem">Item</div></div><div role="table">...</div><div role="img" aria-label="Description">...</div><div role="tooltip">Helpful information</div>
<!-- Landmark roles (prefer semantic HTML elements) --><div role="banner"> <!-- prefer <header> --><div role="navigation"> <!-- prefer <nav> --><div role="main"> <!-- prefer <main> --><div role="complementary"> <!-- prefer <aside> --><div role="contentinfo"> <!-- prefer <footer> --><div role="search"> <!-- prefer <search> in modern HTML --><div role="form"> <!-- prefer <form> with accessible name --><div role="region"> <!-- prefer <section> with accessible name -->ARIA States and Properties
States are dynamic and change with user interaction. Properties are more static.
<!-- States (change during interaction) --><button aria-expanded="false">Menu</button><input aria-invalid="true" /><div role="checkbox" aria-checked="mixed"></div><button aria-pressed="true">Bold</button><div aria-hidden="true">Decorative content</div><div aria-disabled="true">Disabled section</div><li aria-selected="true">Selected item</li><div aria-busy="true">Loading...</div>
<!-- Properties (generally static) --><input aria-label="Search" /><input aria-labelledby="label-id" /><input aria-describedby="help-text-id" /><div aria-owns="child-element-id"></div><div aria-controls="panel-id"></div><ul aria-orientation="horizontal"></ul><input aria-required="true" /><input aria-autocomplete="list" /><div aria-live="polite">Dynamic content area</div><div aria-roledescription="slide">Carousel slide</div>Accessible Names
Every interactive element needs an accessible name — the text a screen reader announces:
<!-- Method 1: Visible label (preferred) --><label for="email">Email address</label><input id="email" type="email" />
<!-- Method 2: aria-label (when no visible label) --><button aria-label="Close dialog">×</button><input type="search" aria-label="Search products" />
<!-- Method 3: aria-labelledby (reference another element) --><h2 id="section-title">User Settings</h2><form aria-labelledby="section-title">...</form>
<!-- Method 4: title attribute (last resort) --><input type="text" title="Zip code" />
<!-- Priority order for accessible name calculation: 1. aria-labelledby 2. aria-label 3. <label> element 4. title attribute 5. placeholder (NOT recommended as sole label) -->Landmark Regions
Landmarks provide a way for screen reader users to quickly navigate to major sections of a page:
<body> <header> <!-- banner landmark --> <nav aria-label="Main"> <!-- navigation landmark --> ... </nav> </header>
<nav aria-label="Breadcrumb"> <!-- another navigation landmark --> ... </nav>
<main> <!-- main landmark (only one per page) --> <section aria-labelledby="intro-heading"> <!-- region landmark --> <h2 id="intro-heading">Introduction</h2> ... </section>
<form aria-labelledby="search-heading"> <!-- form landmark --> <h2 id="search-heading">Search</h2> ... </form> </main>
<aside> <!-- complementary landmark --> ... </aside>
<footer> <!-- contentinfo landmark --> <nav aria-label="Footer"> <!-- navigation landmark --> ... </nav> </footer></body>Live Regions
Live regions announce dynamic content changes to screen readers without requiring the user to move focus.
aria-live Values
| Value | Behavior | Use Case |
|---|---|---|
off | Changes not announced | Default |
polite | Announced after current task | Status updates, non-urgent messages |
assertive | Announced immediately, interrupting current speech | Errors, critical alerts |
Examples
<!-- Polite: status message (announced after current speech) --><div aria-live="polite" aria-atomic="true"> <!-- Content dynamically updated by JavaScript --> 3 results found</div>
<!-- Assertive: error alert (announced immediately) --><div role="alert"> <!-- role="alert" implies aria-live="assertive" --> Payment failed. Please check your card details.</div>
<!-- Status: non-critical information --><div role="status"> <!-- role="status" implies aria-live="polite" --> File uploaded successfully.</div>
<!-- Log: sequential information --><div role="log" aria-live="polite"> <!-- New entries added dynamically --> <p>User joined the chat</p> <p>Message sent</p></div>Live Region Attributes
<div aria-live="polite" aria-atomic="true" <!-- Announce entire region, not just changed part --> aria-relevant="additions" <!-- What types of changes to announce: additions, removals, text, all -->> Updated content here</div>Common ARIA Patterns
Tabs
<div role="tablist" aria-label="Product information"> <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1" tabindex="0" > Description </button> <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1" > Reviews </button> <button role="tab" id="tab-3" aria-selected="false" aria-controls="panel-3" tabindex="-1" > Shipping </button></div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0"> <p>Product description content...</p></div><div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden> <p>Reviews content...</p></div><div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0" hidden> <p>Shipping content...</p></div>Keyboard interaction:
Tabto move to the tab list, then into the active panelArrow Left/Rightto move between tabsHome/Endto move to first/last tab
Modal Dialog
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc"> <h2 id="dialog-title">Delete Account</h2> <p id="dialog-desc"> Are you sure you want to delete your account? This action cannot be undone. </p> <div class="dialog-actions"> <button onclick="closeDialog()">Cancel</button> <button onclick="deleteAccount()">Delete</button> </div></div>Requirements:
- Focus is trapped inside the dialog (Tab cycles through dialog elements)
Escapecloses the dialog- Focus returns to the element that opened the dialog
- Content behind the dialog is inert (
aria-hidden="true"orinertattribute)
Menu
<button aria-haspopup="true" aria-expanded="false" aria-controls="user-menu" id="menu-button"> User Menu</button>
<ul role="menu" id="user-menu" aria-labelledby="menu-button" hidden> <li role="menuitem"><a href="/profile">Profile</a></li> <li role="menuitem"><a href="/settings">Settings</a></li> <li role="separator"></li> <li role="menuitem"><a href="/logout">Sign Out</a></li></ul>Keyboard interaction:
Enter/Spaceopens the menuArrow Down/Upnavigates between itemsEscapecloses the menu and returns focus to the trigger- Type-ahead: typing a letter jumps to the next item starting with that letter
Accordion
<div class="accordion"> <h3> <button aria-expanded="true" aria-controls="section1-content" id="section1-header" > Section 1 </button> </h3> <div id="section1-content" role="region" aria-labelledby="section1-header" > <p>Section 1 content...</p> </div>
<h3> <button aria-expanded="false" aria-controls="section2-content" id="section2-header" > Section 2 </button> </h3> <div id="section2-content" role="region" aria-labelledby="section2-header" hidden > <p>Section 2 content...</p> </div></div>When NOT to Use ARIA
ARIA should be your last resort, not your first tool. Here are common cases where native HTML is better:
| Instead of ARIA | Use Native HTML |
|---|---|
<div role="button"> | <button> |
<div role="link"> | <a href="..."> |
<div role="heading" aria-level="2"> | <h2> |
<div role="list"><div role="listitem"> | <ul><li> |
<div role="checkbox" aria-checked="false"> | <input type="checkbox"> |
<div role="textbox"> | <input type="text"> or <textarea> |
<div role="navigation"> | <nav> |
<div role="main"> | <main> |
<div role="img" aria-label="..."> | <img alt="..."> |
Native HTML elements come with:
- Built-in keyboard interaction
- Built-in focus management
- Built-in accessibility semantics
- Built-in form validation
- No JavaScript required