
Accessibility Audit
- 130 installs
- 47 repo stars
- Updated July 6, 2026
- cuellarfr/design-skills
Audits pages, components, and flows against WCAG 2.2 at A, AA, and AAA, assessing severity and providing code fixes and reports.
About
Conducts accessibility audits grounded in WCAG 2.2 and POUR principles, covering automated and manual testing, keyboard, screen readers, contrast, and semantic HTML. A developer uses it to find accessibility barriers, prioritize them, and write compliant fixes.
- WCAG 2.2 A/AA/AAA conformance and severity guidance
- Covers keyboard, screen reader, contrast, and semantic HTML
Accessibility Audit by the numbers
- 130 all-time installs (skills.sh)
- Ranked #1,043 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cuellarfr/design-skills --skill accessibility-auditAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 130 |
|---|---|
| repo stars | ★ 47 |
| Last updated | July 6, 2026 |
| Repository | cuellarfr/design-skills ↗ |
What it does
Audits pages, components, and flows against WCAG 2.2 at A, AA, and AAA, assessing severity and providing code fixes and reports.
Files
Accessibility Audit
You are an expert in web accessibility and WCAG 2.2 conformance. You help teams audit digital products, identify barriers, write compliant code, and build inclusive experiences.
Your work is grounded in WCAG 2.2, WebAIM guidelines, and the four principles of accessibility: Perceivable, Operable, Understandable, and Robust (POUR).
Core Principle
Accessibility is not a feature — it is a baseline quality requirement. Good accessibility benefits all users, not just those with disabilities. Every interaction you design or code should work for people using keyboards, screen readers, voice control, switch devices, and magnification.
---
WCAG Conformance Levels
| Level | What It Means | Legal Requirement | Target |
|---|---|---|---|
| A | Minimum — removes the most severe barriers | Usually required by law | Absolute floor |
| AA | Standard — addresses the majority of barriers | Most common legal standard (ADA, EN 301 549, EAA) | Default target for all projects |
| AAA | Enhanced — highest level of accessibility | Rarely required by law; aspirational | Specific content or features |
Rule of thumb: Target AA conformance for everything. Apply AAA criteria where practical, especially for text content, color contrast, and target sizes.
What's New in WCAG 2.2 (vs 2.1)
| Criterion | Level | What It Adds |
|---|---|---|
| 2.4.11 Focus Not Obscured (Minimum) | AA | Focused element must not be entirely hidden by sticky headers/modals |
| 2.4.12 Focus Not Obscured (Enhanced) | AAA | Focused element must be fully visible |
| 2.4.13 Focus Appearance | AAA | Custom focus indicators must meet size and contrast requirements |
| 2.5.7 Dragging Movements | AA | Drag operations must have non-drag alternatives |
| 2.5.8 Target Size (Minimum) | AA | Touch targets at least 24×24px (or spaced to avoid overlap) |
| 3.2.6 Consistent Help | A | Help mechanisms must appear in consistent locations |
| 3.3.7 Redundant Entry | A | Don't ask users to re-enter information in a single session |
| 3.3.8 Accessible Authentication (Minimum) | AA | No cognitive function tests (CAPTCHAs, puzzles) without alternatives |
| 3.3.9 Accessible Authentication (Enhanced) | AAA | Stricter authentication requirements |
---
How to Run an Accessibility Audit
The 5-Layer Audit Process
Run these layers in order. Each layer catches different types of issues.
Layer 1: Automated Scan (10 minutes)
Run automated tools first — they catch ~30-40% of WCAG issues instantly.
Tools:
- axe DevTools (browser extension) — Industry standard, low false-positive rate
- WAVE (browser extension) — Visual overlay showing issues in context
- Lighthouse (Chrome DevTools → Accessibility) — Quick score with issue list
- Pa11y (CLI) — CI/CD integration for automated regression testing
What automated tools catch: Missing alt text, color contrast failures, missing labels, incorrect ARIA, heading hierarchy issues, missing lang attribute, empty buttons/links.
What automated tools miss: Whether alt text is meaningful, keyboard trap detection, logical focus order, whether error messages are helpful, content comprehension, screen reader announcement quality.
Layer 2: Keyboard Navigation (15 minutes)
Put your mouse away. Navigate the entire page/flow using only the keyboard.
| Key | Action |
|---|---|
Tab | Move to next interactive element |
Shift + Tab | Move to previous interactive element |
Enter | Activate link or button |
Space | Activate button, toggle checkbox, open select |
Arrow keys | Navigate within components (tabs, menus, radio groups) |
Escape | Close modal, dismiss popover |
Check for:
- [ ] Can you reach every interactive element?
- [ ] Is the focus order logical (left-to-right, top-to-bottom)?
- [ ] Is focus always visible? (look for a clear outline or highlight)
- [ ] Can you operate every control (buttons, links, dropdowns, tabs, modals)?
- [ ] Can you escape from modals and popovers?
- [ ] Are you ever trapped (can't Tab away from an element)?
- [ ] Does focus go to the right place after actions (modal open, form submit, delete)?
- [ ] Is the focused element ever hidden behind a sticky header or modal?
Layer 3: Screen Reader Testing (20 minutes)
Test with at least one screen reader. VoiceOver (macOS) or NVDA (Windows) are free.
VoiceOver quick start (macOS):
Cmd + F5— Toggle VoiceOver on/offVO + Right Arrow— Move to next element (VO=Ctrl + Option)VO + Space— Activate elementVO + U— Open rotor (headings, links, landmarks)
Check for:
- [ ] Are images announced with meaningful descriptions?
- [ ] Are form fields announced with their labels?
- [ ] Are buttons and links announced with their purpose?
- [ ] Are headings properly nested (h1 → h2 → h3)?
- [ ] Are page landmarks present (banner, navigation, main, contentinfo)?
- [ ] Are status messages announced (success, error, loading)?
- [ ] Are decorative images hidden from screen readers?
- [ ] Do custom components announce their role and state?
Layer 4: Visual and Content Review (15 minutes)
Inspect visual presentation and content quality.
Color and contrast:
- [ ] Text contrast ≥ 4.5:1 (or ≥ 3:1 for large text: 18pt+ or 14pt+ bold)
- [ ] Non-text contrast ≥ 3:1 (icons, borders, focus indicators, chart elements)
- [ ] Color is never the only way to convey information (add icons, patterns, or text)
- [ ] Links are distinguishable from body text (underline, or 3:1 contrast + hover/focus change)
Text and readability:
- [ ] Text is resizable to 200% without loss of content or function
- [ ] Page reflows at 320px width (400% zoom) without horizontal scrolling
- [ ] Text spacing can be increased (line-height 1.5×, paragraph spacing 2×, word spacing 0.16×, letter spacing 0.12×) without breaking layout
- [ ] Content is written at appropriate reading level
Interactive elements:
- [ ] Touch targets are at least 24×24px (AA) or 44×44px (AAA)
- [ ] Hover/focus content is dismissible (Esc), hoverable, and persistent
- [ ] Drag operations have non-drag alternatives
Layer 5: Flow and Context Testing (15 minutes)
Test complete user flows, not just individual pages.
- [ ] Forms: Are errors identified, described, and easy to fix?
- [ ] Forms: Are required fields indicated before submission?
- [ ] Forms: Is redundant entry avoided (don't ask for the same info twice)?
- [ ] Authentication: Can users log in without cognitive function tests?
- [ ] Navigation: Is help consistently placed across pages?
- [ ] Navigation: Are skip links present?
- [ ] Navigation: Are multiple ways to find content available (search, nav, sitemap)?
- [ ] Time limits: Can users extend, adjust, or turn off time limits?
- [ ] Media: Do videos have captions? Audio-only has transcripts?
- [ ] Motion: Can animations be paused? Is
prefers-reduced-motionrespected?
---
Severity Scoring
Score each issue to prioritize fixes.
Impact Scale
| Severity | Definition | Example |
|---|---|---|
| Critical | Completely blocks access for some users. Legal risk. | Missing form labels (screen reader users can't fill forms), keyboard trap |
| High | Significantly impairs use. Major frustration. | Poor contrast on primary text, no skip navigation, missing alt text on functional images |
| Moderate | Causes difficulty but workarounds exist. | Decorative images with non-empty alt text, inconsistent heading levels, low contrast on secondary UI |
| Low | Minor inconvenience. Best practice violation. | Missing lang on inline foreign text, suboptimal ARIA usage |
WCAG Level as Priority Signal
| WCAG Level | Default Priority |
|---|---|
| A violations | Critical or High — these are the floor |
| AA violations | High or Moderate — the standard target |
| AAA violations | Moderate or Low — aspirational improvements |
Triage Framework
| Tier | Criteria | Action |
|---|---|---|
| Tier 1 | Critical/High + Level A or AA | Fix immediately — before next release |
| Tier 2 | Moderate + Level AA | Fix in current or next sprint |
| Tier 3 | Low + Level AAA or best practice | Add to backlog |
---
The 80/20: Most Common Issues
These 10 issues account for the vast majority of accessibility failures (based on WebAIM Million analysis). Fix these first.
| # | Issue | WCAG | Level | Frequency |
|---|---|---|---|---|
| 1 | Low text contrast | 1.4.3 | AA | 83% of pages |
| 2 | Missing alt text on images | 1.1.1 | A | 55% of pages |
| 3 | Missing form input labels | 1.1.1, 1.3.1 | A | 46% of pages |
| 4 | Empty links (no text) | 2.4.4 | A | 44% of pages |
| 5 | Empty buttons (no text) | 2.4.4 | A | 27% of pages |
| 6 | Missing document language | 3.1.1 | A | 18% of pages |
| 7 | Missing or broken skip navigation | 2.4.1 | A | Common |
| 8 | No visible focus indicator | 2.4.7 | AA | Common |
| 9 | Incorrect heading hierarchy | 1.3.1 | A | Common |
| 10 | Inaccessible custom components (no ARIA) | 4.1.2 | A | Common |
For code fixes for each of these, see references/common-issues-and-fixes.md.
---
Semantic HTML: The Foundation
Semantic HTML gives you ~70% of accessibility for free. Before reaching for ARIA, use the right HTML element.
| Instead of... | Use... | Why |
|---|---|---|
<div onclick="..."> | <button> | Gets keyboard support, role, and focus for free |
<span class="link"> | <a href="..."> | Announced as link, keyboard navigable |
<div class="header"> | <header>, <nav>, <main>, <footer> | Creates landmarks for screen reader navigation |
<div class="heading"> | <h1> – <h6> | Creates heading hierarchy for navigation |
<div class="list"> | <ul>, <ol>, <li> | Announced as list with item count |
<div class="table"> | <table>, <th>, <td> | Associates headers with data cells |
<div class="input"> | <input>, <select>, <textarea> | Native form behavior, labels, validation |
The first rule of ARIA: Don't use ARIA if native HTML can do the job. ARIA overrides native semantics and is easy to get wrong.
---
Quick Audit Checklist (For Code Reviews)
Use this during pull requests to catch issues before they ship.
HTML Structure
- [ ] Page has one
<h1>, headings don't skip levels - [ ]
<html lang="...">is set - [ ] Landmarks present:
<header>,<nav>,<main>,<footer> - [ ] Skip link targets
<main>or primary content area - [ ] Semantic elements used (not
<div>for everything)
Images
- [ ] Functional images have descriptive
alttext - [ ] Decorative images have
alt=""or are CSS backgrounds - [ ] Complex images (charts, diagrams) have extended descriptions
Forms
- [ ] Every input has a visible
<label>(oraria-labelif visually hidden) - [ ] Required fields are indicated (not by color alone)
- [ ] Error messages identify the field and describe the error
- [ ]
autocompleteattributes set for personal data fields - [ ] Related fields grouped with
<fieldset>and<legend>
Interactive Elements
- [ ] All functionality available via keyboard
- [ ] Focus order matches visual order
- [ ] Focus indicator is visible (not
outline: nonewithout replacement) - [ ] Custom components have correct ARIA roles, states, and properties
- [ ] Modals trap focus and return focus on close
Color and Contrast
- [ ] Text contrast ≥ 4.5:1 (≥ 3:1 for large text)
- [ ] UI component contrast ≥ 3:1
- [ ] Information not conveyed by color alone
Dynamic Content
- [ ] Status messages use
aria-liveorrole="alert" - [ ] Loading states are announced
- [ ] Content added to the DOM is reachable by keyboard and screen reader
---
Reference Files
references/wcag-checklist.md— Complete WCAG 2.2 checklist organized by audit task (what to check, not WCAG principle order)references/common-issues-and-fixes.md— Top 15 issues with before/after code examplesreferences/testing-tools-and-techniques.md— Automated tools, manual techniques, screen reader commands, CI/CD integration
Templates
templates/audit-report-template.md— Full audit report with executive summary, findings, and prioritized recommendationstemplates/quick-checklist-template.md— Single-page checklist for sprint reviews or PR reviews
Examples
examples/audit-walkthrough.md— End-to-end audit of a fictional e-commerce checkout pageexamples/quick-wins.md— The fixes that solve the most common accessibility problems with minimal effort
Accessibility Audit Walkthrough
This example walks through auditing a fictional e-commerce checkout page for "GreenLeaf," an organic grocery delivery service. We'll run the 5-layer audit process and produce prioritized findings.
---
Context
Page: GreenLeaf checkout (cart → shipping → payment → confirmation) Target: WCAG 2.2 Level AA Tools: axe DevTools, VoiceOver (macOS), Chrome DevTools Date: March 2026
---
Layer 1: Automated Scan (axe DevTools)
Running axe DevTools on the checkout page returns:
| # | Issue | WCAG | Impact | Elements |
|---|---|---|---|---|
| 1 | Color contrast insufficient | 1.4.3 | Serious | 7 elements (subtotal labels, help text, placeholder text) |
| 2 | Form elements do not have associated labels | 1.3.1 | Critical | 3 elements (card number, expiry, CVV) |
| 3 | Images do not have alt attributes | 1.1.1 | Critical | 2 elements (product thumbnails in cart) |
| 4 | Links do not have discernible text | 2.4.4 | Serious | 1 element (social login icon) |
| 5 | Document does not have a main landmark | 1.3.1 | Moderate | Page |
| 6 | Page does not contain a level-one heading | 1.3.1 | Moderate | Page |
Lighthouse accessibility score: 62/100
---
Layer 2: Keyboard Navigation
| Step | Result | Issue? |
|---|---|---|
| Tab from page top | Focus starts at logo link | OK |
| Skip link? | No skip link present | Issue: Missing skip link |
| Tab through header nav | 8 nav links, all reachable | OK |
| Tab to cart items | Can reach "Remove" buttons | OK |
| Tab to quantity fields | Focus visible but low contrast | Issue: Low-contrast focus ring |
| Tab to promo code field | Reachable | OK |
| Tab to shipping form | Address fields reachable | OK |
| Tab to payment fields | Card fields inside iframe, focus works | OK |
| Tab to "Place Order" button | Reachable, Enter activates | OK |
| Tab past "Place Order" | Focus goes to footer, logical | OK |
| Open "Edit address" modal | Focus does NOT move to modal | Issue: Focus not managed on modal open |
| Tab inside modal | Focus escapes to page behind modal | Issue: Focus not trapped in modal |
| Press Escape in modal | Nothing happens | Issue: Modal doesn't close on Escape |
| Close modal via X button | Focus does not return to trigger | Issue: Focus not restored on close |
---
Layer 3: Screen Reader Testing (VoiceOver)
| Check | Result | Issue? |
|---|---|---|
| Headings (Rotor) | No h1 found. Section headings are styled <div>s, not <h2>s | Issue: No semantic headings |
| Landmarks (Rotor) | <nav> present, no <main>, no <footer> | Issue: Missing landmarks |
| Cart product images | "Image" announced with no description | Confirmed: Missing alt text |
| Quantity input | "Stepper" — no label announced | Issue: Unlabeled quantity input |
| "Remove" button | "Button: Remove" — which item? Ambiguous | Issue: Ambiguous button label |
| Shipping address fields | Labels announced correctly | OK |
| Payment card fields | "Edit text" — no label announced | Confirmed: Missing card field labels |
| "Place Order" button | "Button: Place Order" | OK |
| Order summary prices | Read correctly | OK |
| Error on empty required field | Red border appears, no announcement | Issue: Errors not announced |
| Promo code success message | "Promo applied" toast appears visually, not announced | Issue: Status message not announced |
---
Layer 4: Visual Review
| Check | Result | Issue? |
|---|---|---|
| Text contrast (body text) | #333 on #fff = 12.6:1 | OK |
| Text contrast (help text) | #aaa on #fff = 2.3:1 | Confirmed: Fails 4.5:1 |
| Text contrast (placeholder) | #ccc on #fff = 1.6:1 | Confirmed: Fails 4.5:1 |
| Subtotal labels | #888 on #f5f5f5 = 3.5:1 | Fails 4.5:1 for normal text |
| Focus indicator | 1px light blue outline on white background | Issue: Low contrast focus ring (~2:1) |
| Color as sole indicator | Error fields: red border only, no icon or text | Issue: Color-only error indication |
| Touch targets (mobile) | "Remove" links are 12×16px | Issue: Fails 24×24px minimum |
| 200% zoom | Layout reflows, some overlap in payment section | Issue: Overlap at 200% zoom |
| 320px reflow | Cart table requires horizontal scroll | OK (tables exempt) |
| Text spacing test | Promo code button text gets clipped | Issue: Text clipped with increased spacing |
---
Layer 5: Flow Testing
| Check | Result | Issue? |
|---|---|---|
| Complete checkout (keyboard only) | Possible but modal issues block address editing | Blocked by modal issues |
| Form errors (submit empty) | Red borders appear, no error summary, no field-level messages | Issue: Inadequate error handling |
| Authentication | Guest checkout available, no CAPTCHA | OK |
| Help location | Help link in footer, consistent with other pages | OK |
| Redundant entry | Billing address defaults to shipping (can uncheck) | OK |
---
Compiled Findings
Finding 1: Payment Card Fields Missing Labels
Severity: Critical WCAG: 1.3.1 (A), 1.1.1 (A) Affects: Screen reader users — cannot determine which field is for card number, expiry, or CVV
Current:
<input type="text" class="card-number" placeholder="Card number">
<input type="text" class="expiry" placeholder="MM/YY">
<input type="text" class="cvv" placeholder="CVV">Fix:
<label for="card-number">Card number</label>
<input type="text" id="card-number" autocomplete="cc-number" inputmode="numeric">
<label for="card-expiry">Expiration date (MM/YY)</label>
<input type="text" id="card-expiry" autocomplete="cc-exp">
<label for="card-cvv">Security code (CVV)</label>
<input type="text" id="card-cvv" autocomplete="cc-csc" inputmode="numeric">Effort: Small
---
Finding 2: Edit Address Modal Not Accessible
Severity: Critical WCAG: 2.1.2 (A), 2.4.3 (A), 4.1.2 (A) Affects: Keyboard users — cannot use the modal properly; may get trapped or lose their place
Issues:
- Focus doesn't move to modal on open
- Focus isn't trapped within modal
- Escape doesn't close modal
- Focus doesn't return to trigger on close
- Modal not announced as dialog
Fix:
<dialog id="edit-address" aria-labelledby="modal-title">
<h2 id="modal-title">Edit shipping address</h2>
<!-- form fields -->
<button>Save</button>
<button>Cancel</button>
</dialog>Using the native <dialog> element with showModal() handles focus trapping, Escape, and backdrop automatically. If using a custom implementation, all four focus behaviors must be manually coded.
Effort: Medium
---
Finding 3: Low Contrast Text Throughout
Severity: High WCAG: 1.4.3 (AA) Affects: Low vision users, users in bright environments — 7 elements fail minimum contrast
| Element | Current | Ratio | Required | Fix |
|---|---|---|---|---|
| Help text | #aaa on #fff | 2.3:1 | 4.5:1 | Change to #595959 |
| Placeholder text | #ccc on #fff | 1.6:1 | 4.5:1 | Change to #767676 |
| Subtotal labels | #888 on #f5f5f5 | 3.5:1 | 4.5:1 | Change to #595959 or darken bg |
Effort: Small
---
Finding 4: No Semantic Structure
Severity: High WCAG: 1.3.1 (A) Affects: Screen reader users — cannot navigate by headings or landmarks
Issues:
- No
<h1>on the page - Section titles ("Your Cart," "Shipping," "Payment") are
<div>s, not headings - No
<main>landmark - No
<footer>landmark
Fix:
<main>
<h1>Checkout</h1>
<section aria-labelledby="cart-heading">
<h2 id="cart-heading">Your Cart</h2>
<!-- cart items -->
</section>
<section aria-labelledby="shipping-heading">
<h2 id="shipping-heading">Shipping</h2>
<!-- shipping form -->
</section>
<section aria-labelledby="payment-heading">
<h2 id="payment-heading">Payment</h2>
<!-- payment form -->
</section>
</main>Effort: Small
---
Finding 5: Form Errors Not Accessible
Severity: High WCAG: 3.3.1 (A), 3.3.3 (AA) Affects: Screen reader users — errors not announced; all users — no guidance on how to fix
Current behavior: Red border on invalid fields, no text message, no announcement.
Fix:
<!-- Error summary at top of form -->
<div role="alert" aria-labelledby="error-heading">
<h3 id="error-heading">Please fix 2 errors:</h3>
<ul>
<li><a href="#card-number">Card number is required</a></li>
<li><a href="#card-expiry">Enter a valid expiration date (MM/YY)</a></li>
</ul>
</div>
<!-- Per-field error -->
<label for="card-number">Card number</label>
<input type="text" id="card-number" aria-invalid="true" aria-describedby="card-error">
<span id="card-error" class="error">Card number is required</span>Effort: Medium
---
Finding 6: Missing Product Image Alt Text
Severity: Moderate WCAG: 1.1.1 (A) Affects: Screen reader users — can't identify items in cart
Fix: <img src="tomatoes.jpg" alt="Organic vine tomatoes, 1 lb">
Effort: Small
---
Finding 7: Ambiguous "Remove" Buttons
Severity: Moderate WCAG: 2.4.4 (A) Affects: Screen reader users — all buttons say "Remove" with no context
Fix: <button aria-label="Remove Organic vine tomatoes from cart">Remove</button>
Effort: Small
---
Prioritized Remediation Plan
| # | Finding | WCAG | Severity | Effort | Tier |
|---|---|---|---|---|---|
| 1 | Payment fields missing labels | 1.3.1 (A) | Critical | Small | 1 |
| 2 | Modal not accessible | 2.1.2 (A) | Critical | Medium | 1 |
| 3 | Low contrast text | 1.4.3 (AA) | High | Small | 1 |
| 4 | No semantic structure | 1.3.1 (A) | High | Small | 1 |
| 5 | Errors not accessible | 3.3.1 (A) | High | Medium | 1 |
| 6 | Missing image alt text | 1.1.1 (A) | Moderate | Small | 2 |
| 7 | Ambiguous button labels | 2.4.4 (A) | Moderate | Small | 2 |
| 8 | Missing skip link | 2.4.1 (A) | Moderate | Small | 2 |
| 9 | Low contrast focus ring | 2.4.7 (AA) | Moderate | Small | 2 |
| 10 | Small touch targets | 2.5.8 (AA) | Moderate | Small | 2 |
| 11 | Status messages not announced | 4.1.3 (AA) | Moderate | Small | 2 |
| 12 | Color-only error indication | 1.4.1 (A) | Moderate | Small | 2 |
| 13 | Text clipped with spacing | 1.4.12 (AA) | Low | Small | 3 |
| 14 | Overlap at 200% zoom | 1.4.4 (AA) | Low | Medium | 3 |
Estimated remediation time: Tier 1 fixes: ~2 days. Tier 2: ~1 day. Tier 3: ~0.5 day.
---
What This Audit Demonstrated
1. 5-layer approach catches what single-method testing misses — Automated found 6 issues; the full audit found 14 2. Automated tools are necessary but not sufficient — axe missed the modal issues, ambiguous labels, error handling, and focus management problems 3. Keyboard testing reveals interaction failures — The modal issues are critical blockers that only Tab testing exposed 4. Screen reader testing reveals content failures — Missing headings and ambiguous labels only surface when you hear the page announced 5. Every finding has a specific code fix — Not "improve accessibility," but "add <label for='card-number'> to the card number input"
Accessibility Quick Wins
The 20% of fixes that solve 80% of accessibility problems. Each fix is small effort, high impact. Sorted by expected improvement to your accessibility score.
---
1. Add lang to <html>
Time: 30 seconds WCAG: 3.1.1 (A) Impact: Screen readers use correct pronunciation rules for the entire page
<!-- Before -->
<html>
<!-- After -->
<html lang="en">If your page includes content in other languages:
<p>The German word <span lang="de">Schadenfreude</span> has no English equivalent.</p>---
2. Fix Color Contrast
Time: 15-30 minutes (site-wide) WCAG: 1.4.3 (AA) Impact: Readable text for low vision users, outdoor usage, aging population
The safe text colors on white (#fff):
| Use Case | Minimum Safe Color | Ratio |
|---|---|---|
| Body text | #595959 | 7.0:1 (passes AAA) |
| Secondary text | #767676 | 4.5:1 (passes AA) |
| Large text (18pt+) | #949494 | 3.0:1 (passes AA for large) |
| Placeholder text | #767676 | 4.5:1 (passes AA) |
Quick fix: Search your CSS for any color lighter than #767676 used for text on white backgrounds. Darken them.
---
3. Add Labels to All Form Inputs
Time: 5-15 minutes per form WCAG: 1.3.1 (A) Impact: Screen reader users can identify every form field
Pattern to use everywhere:
<label for="fieldname">Label text</label>
<input type="text" id="fieldname">Checklist:
- Search code for
<input>without matching<label for="..."> - Search for inputs relying on
placeholderas the only label - Search for
<textarea>and<select>without labels
---
4. Add Alt Text to Images
Time: 2-5 minutes per page WCAG: 1.1.1 (A) Impact: Screen reader users understand image content
Quick decision:
- Decorative? →
alt="" - Conveys information? → Describe the content:
alt="Bar chart showing revenue grew 40% in Q3" - Is a link/button? → Describe the action:
alt="Download report as PDF"
Find all images missing alt: Run axe DevTools or search code for <img without alt=.
---
5. Add a Skip Link
Time: 10 minutes WCAG: 2.4.1 (A) Impact: Keyboard users skip past navigation on every page
<!-- First element inside <body> -->
<a href="#main" class="skip-link">Skip to main content</a>
<!-- Your navigation -->
<nav>...</nav>
<!-- Main content -->
<main id="main">...</main>.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 1000;
}
.skip-link:focus {
top: 0;
}---
6. Use Semantic Landmarks
Time: 10-20 minutes WCAG: 1.3.1 (A) Impact: Screen reader users can jump between page sections
Replace:
<div class="header">...</div>
<div class="nav">...</div>
<div class="content">...</div>
<div class="sidebar">...</div>
<div class="footer">...</div>With:
<header>...</header>
<nav aria-label="Main">...</nav>
<main>...</main>
<aside>...</aside>
<footer>...</footer>---
7. Fix Heading Hierarchy
Time: 10-15 minutes per page WCAG: 1.3.1 (A) Impact: Screen reader users navigate by heading structure
Rules:
- One
<h1>per page (the page title) - Never skip levels going down
- Use headings for structure, CSS for visual size
Quick check: Install the HeadingsMap browser extension — it shows your heading tree instantly.
---
8. Add Visible Focus Indicators
Time: 10 minutes WCAG: 2.4.7 (AA) Impact: Keyboard users can see where they are on the page
/* One rule to fix the whole site */
:focus-visible {
outline: 2px solid #1a73e8;
outline-offset: 2px;
}Find and remove: Search CSS for outline: none or outline: 0. Remove them, or replace with the rule above.
---
9. Make Icon Buttons Accessible
Time: 2 minutes per button WCAG: 4.1.2 (A) Impact: Screen reader users know what every button does
<!-- Before: "button" with no name -->
<button><svg>...</svg></button>
<!-- After: accessible name via aria-label -->
<button aria-label="Close">
<svg aria-hidden="true">...</svg>
</button>Find all: Search for <button> elements that contain only <svg>, <i>, or <img> with no text content.
---
10. Add aria-live to Dynamic Messages
Time: 5 minutes per message type WCAG: 4.1.3 (AA) Impact: Screen reader users are informed of success, error, and loading states
<!-- For success/info messages -->
<div role="status" aria-live="polite">Item added to cart</div>
<!-- For error/urgent messages -->
<div role="alert" aria-live="assertive">Payment failed. Please try again.</div>---
Quick Wins Cheat Sheet
| # | Fix | Time | WCAG | Effort |
|---|---|---|---|---|
| 1 | Add lang to <html> | 30 sec | 3.1.1 A | Trivial |
| 2 | Fix text contrast | 15-30 min | 1.4.3 AA | Small |
| 3 | Label all form inputs | 5-15 min/form | 1.3.1 A | Small |
| 4 | Add alt text to images | 2-5 min/page | 1.1.1 A | Small |
| 5 | Add skip link | 10 min | 2.4.1 A | Small |
| 6 | Use semantic landmarks | 10-20 min | 1.3.1 A | Small |
| 7 | Fix heading hierarchy | 10-15 min/page | 1.3.1 A | Small |
| 8 | Add visible focus indicators | 10 min | 2.4.7 AA | Small |
| 9 | Label icon buttons | 2 min/button | 4.1.2 A | Trivial |
| 10 | Add aria-live to messages | 5 min/type | 4.1.3 AA | Small |
Total estimated time for all 10: 2-4 hours for a typical site. Expected Lighthouse score improvement: +20-30 points. WCAG issues resolved: Typically 60-70% of all Level A and AA failures.
Common Accessibility Issues and Fixes
The 15 most frequent accessibility failures with before/after code examples. Ordered by prevalence (based on WebAIM Million analysis).
---
1. Low Text Contrast
WCAG: 1.4.3 (AA) | Prevalence: 83% of pages | Severity: High
The #1 accessibility issue on the web. Usually caused by light gray text on white, or colored text on colored backgrounds.
Before (fails):
/* Gray text on white — ratio 2.7:1 */
.subtitle {
color: #999999;
background: #ffffff;
}After (passes):
/* Darkened gray — ratio 4.6:1 */
.subtitle {
color: #595959;
background: #ffffff;
}Quick reference:
| Text Size | AA Minimum | AAA Minimum |
|---|---|---|
| Normal text (<18pt) | 4.5:1 | 7:1 |
| Large text (≥18pt or ≥14pt bold) | 3:1 | 4.5:1 |
| UI components and graphical objects | 3:1 | 3:1 |
Tools: WebAIM Contrast Checker, Chrome DevTools (inspect → color picker shows ratio), axe DevTools.
---
2. Missing Image Alt Text
WCAG: 1.1.1 (A) | Prevalence: 55% of pages | Severity: Critical (functional images) / Low (decorative)
Before (fails):
<!-- No alt attribute at all -->
<img src="product-photo.jpg">
<!-- Empty alt on a functional image -->
<img src="search-icon.svg" alt="">After (passes):
<!-- Descriptive alt for functional image -->
<img src="product-photo.jpg" alt="Blue wireless headphones, side view">
<!-- Empty alt for decorative image -->
<img src="decorative-swoosh.svg" alt="">
<!-- Functional icon with alt describing action -->
<button>
<img src="search-icon.svg" alt="Search">
</button>Alt text decision tree:
1. Is the image decorative (adds no information)? → alt="" 2. Is it a functional image (link, button)? → Describe the action/destination 3. Is it informational? → Describe the content 4. Is it complex (chart, diagram)? → Brief alt + longer description nearby or on linked page 5. Is it an image of text? → Alt text = the text in the image
Common mistakes:
alt="image"oralt="photo"— tells the user nothingalt="IMG_2847.jpg"— filename as alt- Long alt text on decorative images — adds noise for screen reader users
- Missing alt on linked images — link becomes "unlabeled" for screen readers
---
3. Missing Form Labels
WCAG: 1.1.1, 1.3.1 (A) | Prevalence: 46% of pages | Severity: Critical
Screen reader users hear "edit text" with no indication of what to type.
Before (fails):
<!-- Placeholder is not a label -->
<input type="email" placeholder="Enter your email">
<!-- Label not programmatically associated -->
<span>Email</span>
<input type="email">
<!-- Wrapping div breaks association -->
<label>Email</label>
<div>
<input type="email">
</div>After (passes):
<!-- Explicit label with for/id -->
<label for="email">Email address</label>
<input type="email" id="email" autocomplete="email">
<!-- Implicit label (wrapping) -->
<label>
Email address
<input type="email" autocomplete="email">
</label>
<!-- Visually hidden label when design doesn't allow visible label -->
<label for="search" class="visually-hidden">Search</label>
<input type="search" id="search" placeholder="Search...">The visually-hidden class (use this instead of display: none or visibility: hidden, which hide from screen readers too):
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}---
4. Empty Links
WCAG: 2.4.4 (A) | Prevalence: 44% of pages | Severity: High
Links with no text are announced as "link" with no destination — useless for screen reader users.
Before (fails):
<!-- Icon-only link with no accessible name -->
<a href="/settings">
<i class="icon-gear"></i>
</a>
<!-- Image link with no alt -->
<a href="/home">
<img src="logo.png">
</a>After (passes):
<!-- aria-label provides the accessible name -->
<a href="/settings" aria-label="Settings">
<i class="icon-gear" aria-hidden="true"></i>
</a>
<!-- Visually hidden text -->
<a href="/settings">
<i class="icon-gear" aria-hidden="true"></i>
<span class="visually-hidden">Settings</span>
</a>
<!-- Image link with descriptive alt -->
<a href="/home">
<img src="logo.png" alt="Acme Co — Home">
</a>---
5. Empty Buttons
WCAG: 2.4.4 (A) | Prevalence: 27% of pages | Severity: High
Same problem as empty links — buttons with no text are announced as "button" with no purpose.
Before (fails):
<!-- Icon button with no accessible name -->
<button>
<svg><!-- close icon --></svg>
</button>
<!-- Value-less button -->
<button type="submit"></button>After (passes):
<!-- aria-label on button -->
<button aria-label="Close dialog">
<svg aria-hidden="true"><!-- close icon --></svg>
</button>
<!-- Visible text is always best -->
<button type="submit">Submit application</button>---
6. Missing Document Language
WCAG: 3.1.1 (A) | Prevalence: 18% of pages | Severity: Moderate
Without lang, screen readers may use wrong pronunciation rules for the entire page.
Before (fails):
<html>After (passes):
<html lang="en">
<!-- For inline language changes (3.1.2 — AA) -->
<p>The French word <span lang="fr">bonjour</span> means hello.</p>---
7. Missing Skip Navigation
WCAG: 2.4.1 (A) | Severity: High
Keyboard users must Tab through the entire navigation on every page without a skip link.
Before (fails):
<body>
<nav><!-- 20+ navigation links --></nav>
<main><!-- content --></main>
</body>After (passes):
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav><!-- navigation --></nav>
<main id="main-content"><!-- content --></main>
</body>.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 100;
transition: top 0.2s;
}
.skip-link:focus {
top: 0;
}---
8. No Visible Focus Indicator
WCAG: 2.4.7 (AA) | Severity: High
Keyboard users lose track of where they are on the page.
Before (fails):
/* Common CSS reset that kills accessibility */
*:focus {
outline: none;
}
/* Or more targeted but still bad */
button:focus,
a:focus {
outline: none;
}After (passes):
/* Custom focus style that's visible and attractive */
:focus-visible {
outline: 2px solid #1a73e8;
outline-offset: 2px;
border-radius: 2px;
}
/* Remove focus ring for mouse clicks, keep for keyboard */
:focus:not(:focus-visible) {
outline: none;
}Focus indicator requirements (2.4.13 — AAA):
- At least 2px thick perimeter
- At least 3:1 contrast between focused and unfocused states
- Area: (width × 4) + (height × 4) minimum pixels of change
---
9. Incorrect Heading Hierarchy
WCAG: 1.3.1 (A) | Severity: Moderate
Screen reader users navigate by headings. Skipped levels create confusion.
Before (fails):
<h1>Product Page</h1>
<h3>Features</h3> <!-- Skipped h2 -->
<h5>Pricing</h5> <!-- Skipped h4 -->
<h2>Reviews</h2> <!-- Back to h2 — confusing -->After (passes):
<h1>Product Page</h1>
<h2>Features</h2>
<h3>Core Features</h3>
<h3>Advanced Features</h3>
<h2>Pricing</h2>
<h2>Reviews</h2>Rules:
- One
<h1>per page - Never skip levels going down (h1 → h3)
- Can skip levels going back up (h3 → h2 is fine)
- Use headings for structure, not for styling (use CSS for visual size)
---
10. Inaccessible Custom Components
WCAG: 4.1.2 (A) | Severity: Critical
Custom dropdowns, modals, tabs, and toggles that don't expose role, state, or keyboard support.
Before (fails) — Custom dropdown:
<div class="dropdown" onclick="toggle()">
<div class="selected">Choose an option</div>
<div class="options" style="display:none">
<div onclick="select('a')">Option A</div>
<div onclick="select('b')">Option B</div>
</div>
</div>After (passes) — Use native HTML first:
<label for="options">Choose an option</label>
<select id="options">
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>If custom is required — ARIA combobox pattern:
<label id="combo-label">Choose an option</label>
<div role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="combo-label"
tabindex="0">
<span>Choose an option</span>
</div>
<ul role="listbox" aria-labelledby="combo-label" hidden>
<li role="option" id="opt-a">Option A</li>
<li role="option" id="opt-b">Option B</li>
</ul>---
11. Inaccessible Modal Dialogs
WCAG: 2.1.2, 2.4.3, 4.1.2 (A) | Severity: Critical
Modals that don't trap focus, don't return focus, or aren't announced.
After (passes):
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabindex="-1">
<h2 id="dialog-title">Confirm deletion</h2>
<p>Are you sure you want to delete this item?</p>
<button>Cancel</button>
<button>Delete</button>
</div>Required behaviors: 1. Move focus to dialog (or first focusable element) on open 2. Trap Tab cycling within the dialog 3. Close on Escape 4. Return focus to the trigger element on close 5. Make content behind dialog inert (aria-hidden="true" on the rest of the page, or use <dialog> element)
---
12. Missing Status Messages
WCAG: 4.1.3 (AA) | Severity: High
Success messages, error counts, search results — if focus doesn't move there, screen readers don't know about them.
Before (fails):
<!-- Visually appears but screen reader doesn't announce it -->
<div class="toast success">Item saved successfully!</div>After (passes):
<!-- Polite announcement (doesn't interrupt) -->
<div role="status" aria-live="polite">Item saved successfully!</div>
<!-- Assertive announcement (interrupts — use for errors) -->
<div role="alert" aria-live="assertive">
3 errors found. Please fix the highlighted fields.
</div>
<!-- Live region approach — add region first, then inject content -->
<div aria-live="polite" id="search-status"></div>
<script>
document.getElementById('search-status').textContent = '42 results found';
</script>---
13. Insufficient Touch Target Size
WCAG: 2.5.8 (AA, new in 2.2) | Severity: Moderate
Small targets cause mis-taps, especially for users with motor impairments.
Before (fails):
/* 16×16px icon button */
.icon-btn {
width: 16px;
height: 16px;
padding: 0;
}After (passes):
/* Meets AA minimum of 24×24px */
.icon-btn {
min-width: 24px;
min-height: 24px;
padding: 4px; /* visual icon can be smaller, tap target is larger */
}
/* Meets AAA target of 44×44px */
.icon-btn-large {
min-width: 44px;
min-height: 44px;
padding: 10px;
}Exceptions: Inline links in text, browser-default controls, targets where size is essential.
---
14. Focus Hidden Behind Sticky Elements
WCAG: 2.4.11 (AA, new in 2.2) | Severity: Moderate
Sticky headers, footers, or cookie banners can obscure the focused element.
Fix approaches:
/* Ensure scroll-padding accounts for sticky header */
html {
scroll-padding-top: 80px; /* height of sticky header */
}
/* Or use scroll-margin on focusable elements */
:focus-visible {
scroll-margin-top: 80px;
scroll-margin-bottom: 60px; /* height of sticky footer */
}Also check: Cookie consent banners, floating action buttons, chat widgets, notification bars.
---
15. Drag Without Alternative
WCAG: 2.5.7 (AA, new in 2.2) | Severity: Moderate
Drag-and-drop must have non-drag alternatives for users who can't perform dragging movements.
Before (fails):
<!-- Only drag-and-drop to reorder -->
<ul class="sortable">
<li draggable="true">Item 1</li>
<li draggable="true">Item 2</li>
<li draggable="true">Item 3</li>
</ul>After (passes):
<!-- Drag-and-drop PLUS move buttons -->
<ul class="sortable">
<li draggable="true">
Item 1
<button aria-label="Move Item 1 up" disabled>↑</button>
<button aria-label="Move Item 1 down">↓</button>
</li>
<li draggable="true">
Item 2
<button aria-label="Move Item 2 up">↑</button>
<button aria-label="Move Item 2 down">↓</button>
</li>
</ul>Accessibility Testing Tools and Techniques
Automated Testing Tools
Automated tools catch ~30-40% of WCAG issues. They're fast and essential, but never sufficient alone.
Browser Extensions
| Tool | Strengths | Limitations | Best For |
|---|---|---|---|
| axe DevTools | Low false-positive rate, detailed remediation guidance, WCAG reference links | Doesn't test keyboard or screen reader behavior | Developer daily use, CI/CD |
| WAVE | Visual overlay shows issues in context, shows document structure | Can be noisy on complex pages | Quick visual scan, stakeholder demos |
| Lighthouse (Chrome) | Built into DevTools, scores 0-100, tracks over time | Less detailed than axe, limited criteria | Quick health check |
| Accessibility Insights (Microsoft) | Guided manual testing flow, FastPass + full assessment | Windows/Edge focused | Structured audit process |
CLI and CI/CD Tools
| Tool | Integration | Use Case |
|---|---|---|
| axe-core (npm) | Jest, Cypress, Playwright, Storybook | Unit/integration/E2E test suites |
| Pa11y | CLI, CI/CD, dashboard | Automated regression testing |
| Lighthouse CI | GitHub Actions, CI pipelines | Performance + accessibility scoring |
Color Contrast Tools
| Tool | Use Case |
|---|---|
| WebAIM Contrast Checker | Check specific color pairs |
| Chrome DevTools color picker | Shows contrast ratio inline while inspecting |
| Stark (Figma plugin) | Check contrast during design phase |
| Who Can Use | Shows how colors appear to users with different vision types |
Automated Testing Integration Example
// axe-core with Playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no critical accessibility issues', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});// axe-core with Jest and jsdom
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('renders accessible form', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});---
Keyboard Testing
Setup
1. In macOS System Settings → Keyboard, ensure "Keyboard navigation" is on (formerly "Full Keyboard Access") 2. In browser, disable any mouse-tracking extensions 3. Put your mouse out of reach
Test Script
| Step | Action | Check |
|---|---|---|
| 1 | Press Tab repeatedly from the top of the page | Can you reach every interactive element? Is the order logical? |
| 2 | Watch for the focus indicator | Is it always visible? Does it have sufficient contrast? |
| 3 | Press Enter on each link | Does it navigate correctly? |
| 4 | Press Enter and Space on each button | Does it activate? |
| 5 | Navigate to a dropdown/select | Can you open it and select an option with arrow keys? |
| 6 | Navigate to a modal trigger and activate it | Does focus move into the modal? |
| 7 | Inside the modal, press Tab repeatedly | Is focus trapped within the modal? |
| 8 | Press Escape | Does the modal close? Does focus return to the trigger? |
| 9 | Navigate to a tab component | Can you switch tabs with arrow keys? Tab into the panel? |
| 10 | Fill out and submit a form | Can you complete the entire form with keyboard? |
| 11 | If errors appear, can you navigate to each error? | Does focus move to the error summary or first error? |
| 12 | Check for keyboard traps | Can you always Tab away from every element? |
Common Keyboard Failures
| Symptom | Likely Cause | Fix |
|---|---|---|
| Can't reach element via Tab | Element is <div> or <span> without tabindex="0" | Use <button> or <a>, or add tabindex="0" with keyboard event handlers |
| Focus disappears | Focus moved to invisible or off-screen element | Check DOM order, check for display:none or visibility:hidden on focused elements |
| Focus trapped in component | No Escape handler, or Tab not cycling correctly | Add keyboard event listeners for Escape and Tab |
| Custom component doesn't respond to keyboard | Only onclick handler, no onkeydown | Add onkeydown for Enter and Space, or use <button> |
| Focus order doesn't match visual order | CSS reordering (flexbox order, grid, position: absolute) | Align DOM order with visual order |
---
Screen Reader Testing
VoiceOver (macOS) — Free
Essential commands:
| Action | Keys |
|---|---|
| Toggle VoiceOver on/off | Cmd + F5 |
| Move to next element | VO + Right Arrow (VO = Ctrl + Option) |
| Move to previous element | VO + Left Arrow |
| Activate element | VO + Space |
| Read current element | VO + F3 |
| Open Rotor | VO + U |
| Navigate headings | Rotor → Headings, then Up/Down arrows |
| Navigate landmarks | Rotor → Landmarks, then Up/Down arrows |
| Navigate links | Rotor → Links, then Up/Down arrows |
| Navigate form controls | Rotor → Form Controls, then Up/Down arrows |
| Start continuous reading | VO + A |
| Stop reading | Ctrl |
Quick testing flow: 1. Turn on VoiceOver (Cmd + F5) 2. Open the Rotor (VO + U), check headings list — are they logical? 3. Check landmarks list — does the page have banner, navigation, main, contentinfo? 4. Navigate through the page with VO + Right Arrow 5. On images: does VoiceOver read a useful description? 6. On form fields: does VoiceOver announce the label? 7. On buttons/links: does VoiceOver announce the purpose? 8. Submit a form with errors: does VoiceOver announce the errors? 9. Open a modal: does VoiceOver announce the dialog title?
NVDA (Windows) — Free
Essential commands:
| Action | Keys |
|---|---|
| Toggle NVDA | Ctrl + Alt + N |
| Move to next element | Down Arrow (browse mode) |
| Move to previous element | Up Arrow |
| Activate element | Enter |
| Tab to next interactive | Tab |
| List headings | NVDA + F7 → Headings |
| List landmarks | NVDA + F7 → Landmarks |
| Next heading | H |
| Next landmark | D |
| Next form field | F |
| Next button | B |
| Next link | K (unvisited) or V (visited) |
| Switch browse/focus mode | NVDA + Space |
What to Listen For
| Element | Should Announce | Red Flag |
|---|---|---|
| Image | Role + alt text: "Image: Blue headphones" | "Image" (no description) or "IMG_2847.jpg" |
| Link | "Link: Settings" | "Link" (no text) |
| Button | "Button: Submit form" | "Button" (no text) |
| Form field | "Email, edit text" or "Email, required, edit text" | "Edit text" (no label) |
| Heading | "Heading level 2: Features" | No heading announcement |
| Checkbox | "Accept terms, checkbox, not checked" | "Checkbox" (no label) |
| Error | "Error: Email is required" (via alert or live region) | No announcement |
| Modal | "Dialog: Confirm deletion" | No announcement of dialog |
---
Manual Visual Testing
Color Contrast Testing
1. Inspect element in DevTools → click color swatch → see contrast ratio 2. Use WAVE extension → contrast errors highlighted in red 3. Grayscale test: Apply filter: grayscale(100%) to <html> in DevTools — can you still distinguish all elements? 4. Color blindness simulation: Chrome DevTools → Rendering → Emulate vision deficiencies
Zoom and Reflow Testing
| Test | How | Pass Criteria |
|---|---|---|
| 200% zoom | Cmd/Ctrl + + until 200% | Content readable, functional, no overlap |
| 400% zoom / 320px reflow | Set browser to 1280px wide, zoom to 400% | Single column, no horizontal scroll (except tables, maps, toolbars) |
| Text spacing | Use browser extension or bookmarklet to apply: line-height 1.5×, paragraph 2×, word spacing 0.16×, letter spacing 0.12× | No content cut off or overlapping |
Text Spacing Bookmarklet
Save this as a bookmark and click it on any page to test text spacing compliance (1.4.12):
javascript:void(function(){var s=document.createElement('style');s.textContent='*{line-height:1.5em!important;letter-spacing:0.12em!important;word-spacing:0.16em!important}p{margin-bottom:2em!important}';document.head.appendChild(s)})()Reduced Motion Testing
1. macOS: System Settings → Accessibility → Display → Reduce motion 2. Chrome DevTools: Rendering → Emulate CSS media feature prefers-reduced-motion: reduce 3. Check: Do animations stop or simplify? Does the page still work?
---
Testing Cheat Sheet
Quick Audit (30 Minutes)
| Time | Layer | What to Do |
|---|---|---|
| 0-10 min | Automated | Run axe DevTools, note all violations |
| 10-20 min | Keyboard | Tab through entire page, check focus visibility and order |
| 20-25 min | Visual | Check contrast (DevTools), zoom to 200%, check color reliance |
| 25-30 min | Screen Reader | VoiceOver/NVDA quick scan: headings, landmarks, form labels, images |
Full Audit (2-3 Hours Per Page)
| Time | Layer | What to Do |
|---|---|---|
| 0-15 min | Automated | Run axe + WAVE + Lighthouse, compile unique issues |
| 15-35 min | Keyboard | Full keyboard navigation, test all interactive components |
| 35-60 min | Screen Reader | Full page traversal, form completion, error handling, modal interaction |
| 60-80 min | Visual | Contrast check (all states), zoom 200% + 400%, text spacing, reduced motion |
| 80-100 min | Content | Alt text quality, heading structure, link text quality, error message quality |
| 100-120 min | Flow | Complete key user flows (signup, purchase, settings change) with keyboard + screen reader |
| 120-150 min | Documentation | Write findings, assign severity, draft report |
---
Accessibility Testing in Design Phase
Don't wait until code. Catch issues in design:
| Check | Tool/Method | When |
|---|---|---|
| Color contrast | Stark (Figma), WebAIM Contrast Checker | During visual design |
| Touch target size | Measure in design tool (≥24px AA, ≥44px AAA) | During component design |
| Focus states | Design focus indicators for every interactive element | During interaction design |
| Reading order | Mark intended DOM order in design annotations | During layout design |
| Heading hierarchy | Annotate heading levels on mockups | During content design |
| Alternative text | Write alt text in design annotations for every image | During content design |
| Color as information | Check if removing color breaks understanding | During visual design |
| Error states | Design error messages for every form field | During form design |
WCAG 2.2 Checklist — Organized by Audit Task
This checklist reorganizes WCAG 2.2 success criteria by what you're actually checking during an audit, rather than by WCAG principle number. Each section groups related criteria that you'd test together.
---
1. Text Alternatives and Media
Check all non-text content for appropriate alternatives.
Images (1.1.1 — A)
- [ ] Functional images (convey information) have descriptive
alttext - [ ] Decorative images have
alt=""or are implemented as CSS backgrounds - [ ] Linked images have
alttext describing the destination - [ ] Image buttons have
alttext describing the action - [ ] Complex images (charts, infographics) have extended descriptions in context or on a linked page
- [ ] Image map hotspots have appropriate
alttext - [ ] Images of text are not used when real text could achieve the same presentation (1.4.5 — AA)
Audio and Video
| Criterion | Level | Requirement |
|---|---|---|
| 1.2.1 | A | Audio-only: descriptive transcript. Video-only: transcript or audio description |
| 1.2.2 | A | Prerecorded video with audio: synchronized captions |
| 1.2.3 | A | Prerecorded video: transcript or audio description |
| 1.2.4 | AA | Live audio: synchronized captions |
| 1.2.5 | AA | Prerecorded video: audio descriptions |
| 1.2.6 | AAA | Prerecorded video: sign language interpretation |
| 1.2.7 | AAA | Extended audio descriptions when pauses insufficient |
| 1.2.8 | AAA | Prerecorded video: full descriptive transcript |
| 1.2.9 | AAA | Live audio: text transcript (e.g., script) |
Embedded Content
- [ ] Frames and iframes have descriptive
titleattributes (1.1.1 — A) - [ ] Embedded multimedia is identified via accessible text (1.1.1 — A)
---
2. Semantic Structure and Relationships
Check that content structure is programmatically determinable.
Headings (1.3.1 — A)
- [ ] Heading levels don't skip (h1 → h2 → h3, never h1 → h3)
- [ ] Page has exactly one
<h1> - [ ] Headings describe the content that follows
- [ ] Heading levels reflect content hierarchy, not visual styling
- [ ] Section headings are used to organize long content (2.4.10 — AAA)
Landmarks and Regions (1.3.1 — A, 1.3.6 — AAA)
- [ ] Page uses semantic landmarks:
<header>,<nav>,<main>,<footer>,<aside> - [ ] Multiple instances of the same landmark have unique labels (
aria-label) - [ ] ARIA landmark roles are used to enhance HTML where needed (1.3.6 — AAA)
Lists (1.3.1 — A)
- [ ] Related items use
<ul>,<ol>, or<dl>— not styled<div>s - [ ] Navigation menus use list markup
Tables (1.3.1 — A)
- [ ] Data tables use
<th>for headers withscopeattribute - [ ] Complex tables use
id/headersassociations - [ ] Table captions use
<caption>element - [ ] Layout tables (if any) do NOT use
<th>,<caption>, orsummary
Reading Order (1.3.2 — A)
- [ ] DOM order matches visual order
- [ ] CSS doesn't create a reading order that contradicts the DOM
- [ ] Content makes sense when linearized (single column)
Sensory Characteristics (1.3.3 — A)
- [ ] Instructions don't rely solely on shape, size, or position ("click the green button on the right")
- [ ] Instructions don't rely solely on sound ("a beep indicates success")
---
3. Color and Contrast
Text Contrast
| Criterion | Level | Requirement |
|---|---|---|
| 1.4.3 | AA | Normal text: ≥ 4.5:1. Large text (18pt+ or 14pt+ bold): ≥ 3:1 |
| 1.4.6 | AAA | Normal text: ≥ 7:1. Large text: ≥ 4.5:1 |
Non-Text Contrast (1.4.11 — AA)
- [ ] UI components (buttons, inputs, dropdowns): ≥ 3:1 against adjacent colors
- [ ] Graphical objects conveying information (icons, chart segments): ≥ 3:1
- [ ] Focus indicators: ≥ 3:1 against background
- [ ] Component states (hover, active, selected): maintain ≥ 3:1
Color as Information (1.4.1 — A)
- [ ] Color is not the sole method of conveying information
- [ ] Links are distinguishable from body text by more than color alone (underline, or ≥ 3:1 contrast + hover/focus change)
- [ ] Form errors indicated by more than a red border (add icon, text, or pattern)
- [ ] Charts use patterns or labels in addition to color
---
4. Keyboard Access
Full Keyboard Support (2.1.1 — A)
- [ ] All interactive elements are reachable via
Tab - [ ] All interactive elements can be activated via
EnterorSpace - [ ] Custom widgets support expected keyboard patterns (arrows for tabs/menus, Escape to close)
- [ ] No keyboard traps — user can always Tab away (2.1.2 — A)
- [ ] Character key shortcuts can be disabled or remapped (2.1.4 — A)
Focus Management
| Criterion | Level | Requirement |
|---|---|---|
| 2.4.3 | A | Focus order is logical and intuitive |
| 2.4.7 | AA | Focus indicator is visible on all interactive elements |
| 2.4.11 | AA | Focused element not entirely hidden by other content (2.2) |
| 2.4.12 | AAA | Focused element is fully visible (2.2) |
| 2.4.13 | AAA | Custom focus indicator meets size (2px perimeter) and contrast (3:1) requirements (2.2) |
Focus Behavior
- [ ] Focus moves to modals when opened
- [ ] Focus is trapped within open modals (can't Tab to content behind)
- [ ] Focus returns to trigger element when modal closes
- [ ] Focus moves to new content when dynamically added (e.g., error summary)
- [ ] Focus is not moved unexpectedly
---
5. Pointer and Touch Input
Pointer Gestures (2.5.1 — A)
- [ ] Multipoint gestures (pinch, swipe) have single-pointer alternatives
- [ ] Path-based gestures (drag) have click/tap alternatives
Pointer Behavior
| Criterion | Level | Requirement |
|---|---|---|
| 2.5.2 | A | Actions fire on mouseup/touchend, not mousedown/touchstart (cancellable) |
| 2.5.3 | A | Accessible name includes the visible text label |
| 2.5.4 | A | Motion-based activation (shake, tilt) has standard control alternative and can be disabled |
| 2.5.7 | AA | Drag operations have non-drag alternatives (2.2) |
Target Size
| Criterion | Level | Requirement |
|---|---|---|
| 2.5.8 | AA | Targets ≥ 24×24px, or spaced so 24px circles don't overlap (2.2) |
| 2.5.5 | AAA | Targets ≥ 44×44px |
---
6. Forms and Input
Labels and Instructions
- [ ] Every input has a programmatic label:
<label for="...">oraria-label(1.1.1, 1.3.1 — A) - [ ] Related inputs grouped with
<fieldset>and<legend>(1.3.1 — A) - [ ] Required fields indicated in the label (not just by color or asterisk alone) (3.3.2 — A)
- [ ] Input purpose identified with
autocompletefor personal data (1.3.5 — AA) - [ ] Sufficient instructions and examples provided (3.3.2 — A)
Error Handling
| Criterion | Level | Requirement |
|---|---|---|
| 3.3.1 | A | Errors identified and described in text |
| 3.3.3 | AA | Suggestions provided for fixing errors |
| 3.3.4 | AA | Legal/financial/test data: submissions reversible, verified, or confirmed |
| 3.3.6 | AAA | All submissions: reversible, verified, or confirmed |
Error Presentation
- [ ] Error messages identify which field has the error
- [ ] Error messages describe what's wrong and how to fix it
- [ ] Errors are announced to screen readers (via
aria-live,aria-describedby, or focus management) - [ ] Error summary at top of form links to each problematic field
New in 2.2
- [ ] Redundant entry: information is auto-populated or selectable — don't ask twice (3.3.7 — A)
- [ ] Authentication: no cognitive function tests without alternatives (3.3.8 — AA)
---
7. Navigation and Wayfinding
Page-Level
- [ ] Skip link to main content (2.4.1 — A)
- [ ] Descriptive page title: "[Page] — [Site]" (2.4.2 — A)
- [ ]
<html lang="...">set correctly (3.1.1 — A) - [ ] Language changes within page marked with
langattribute (3.1.2 — AA)
Site-Level
- [ ] Multiple navigation methods: nav + search, or nav + sitemap (2.4.5 — AA)
- [ ] Navigation order consistent across pages (3.2.3 — AA)
- [ ] Same functionality labeled consistently across pages (3.2.4 — AA)
- [ ] Help mechanisms in consistent location (3.2.6 — A, new in 2.2)
Links
- [ ] Link text describes the destination (not "click here" or "read more") (2.4.4 — A)
- [ ] Links with identical text go to identical destinations (2.4.4 — A)
- [ ] Link text alone is sufficient to determine purpose (2.4.9 — AAA)
---
8. Time, Motion, and Interruptions
Time Limits (2.2.1 — A)
- [ ] Users can extend, adjust, or turn off time limits
- [ ] Users warned before timeout that could lose data (2.2.6 — AAA)
- [ ] Re-authentication preserves user data (2.2.5 — AAA)
Motion and Animation
- [ ] Auto-playing content (carousels, animations) can be paused/stopped after 5 seconds (2.2.2 — A)
- [ ] No content flashes more than 3 times per second (2.3.1 — A)
- [ ]
prefers-reduced-motionmedia query respected (2.3.3 — AAA) - [ ] Auto-updating content (feeds, tickers) can be paused (2.2.2 — A)
Audio
- [ ] Auto-playing audio (>3 seconds) can be paused, stopped, or volume adjusted (1.4.2 — A)
---
9. Text and Content Presentation
Resizing and Reflow
| Criterion | Level | Requirement |
|---|---|---|
| 1.4.4 | AA | Content readable and functional at 200% zoom |
| 1.4.10 | AA | No horizontal scroll at 320px width (400% zoom) — responsive design |
| 1.4.12 | AA | No content loss when text spacing increased (line-height 1.5×, paragraph 2×, word 0.16×, letter 0.12×) |
Text Presentation (1.4.8 — AAA)
- [ ] Line length ≤ 80 characters
- [ ] Text not fully justified (no left + right alignment)
- [ ] Line spacing ≥ 1.5× font size
- [ ] Paragraph spacing ≥ 1.5× line spacing
- [ ] Foreground and background colors are defined/inheritable
Readability
- [ ] Content written at appropriate reading level (3.1.5 — AAA: ~9th grade)
- [ ] Unusual words defined (3.1.3 — AAA)
- [ ] Abbreviations expanded on first use (3.1.4 — AAA)
---
10. Predictability and Behavior
No Surprise Changes
- [ ] Receiving focus doesn't trigger page changes (3.2.1 — A)
- [ ] Changing an input value doesn't trigger unexpected changes (3.2.2 — A)
- [ ] Major changes only happen on user request (3.2.5 — AAA)
Hover and Focus Content (1.4.13 — AA)
- [ ] Content appearing on hover/focus is dismissible (Esc key)
- [ ] User can hover over the new content without it disappearing
- [ ] Content persists until focus moves, user dismisses it, or it's no longer relevant
---
11. ARIA and Custom Components (4.1.2 — A)
General ARIA Rules
1. Don't use ARIA if native HTML works 2. Don't change native semantics unless necessary 3. All interactive ARIA controls must be keyboard operable 4. Don't use role="presentation" or aria-hidden="true" on focusable elements 5. All interactive elements must have accessible names
Common ARIA Patterns
| Component | Required ARIA | Key Behaviors |
|---|---|---|
| Modal/Dialog | role="dialog", aria-modal="true", aria-labelledby | Focus trap, Esc to close, return focus |
| Tab Panel | role="tablist", role="tab", role="tabpanel", aria-selected | Arrow keys between tabs, Tab into panel |
| Accordion | aria-expanded, aria-controls | Enter/Space to toggle, optional arrow keys |
| Menu | role="menu", role="menuitem", aria-haspopup | Arrow keys to navigate, Esc to close |
| Alert | role="alert" or aria-live="assertive" | Auto-announced by screen readers |
| Status | role="status" or aria-live="polite" | Announced at next pause |
| Toggle | aria-pressed="true/false" | Space/Enter to toggle, state announced |
| Combobox | role="combobox", aria-expanded, aria-activedescendant | Type to filter, arrows to select |
Status Messages (4.1.3 — AA)
- [ ] Success messages announced via
aria-live="polite"orrole="status" - [ ] Error messages announced via
aria-live="assertive"orrole="alert" - [ ] Loading states announced (e.g.,
aria-busy="true"on region, or live region update) - [ ] Search result counts announced
Accessibility Audit Report
Audit Overview
- Product/Page: [What was audited]
- URL(s): [Specific URLs tested]
- Date: [Audit date]
- Auditor: [Name]
- Target conformance: WCAG 2.2 Level [A / AA / AAA]
- Testing methods: [Automated scan, keyboard testing, screen reader testing, visual review]
- Tools used: [axe DevTools, WAVE, VoiceOver, NVDA, Chrome DevTools, etc.]
- Browsers/devices: [Chrome on macOS, Safari on iOS, etc.]
---
Executive Summary
3-5 bullet points that a stakeholder can read in 30 seconds.
- Overall conformance: [Passes / Does not pass] WCAG 2.2 Level [AA]
- Total issues found: [#] ([#] Critical, [#] High, [#] Moderate, [#] Low)
- Top finding: [Most impactful issue — one sentence]
- Biggest quick win: [Easiest high-impact fix — one sentence]
- Recommendation: [Overall next step]
---
Score Summary
By Severity
| Severity | Count | % of Total |
|---|---|---|
| Critical | _ | _% |
| High | _ | _% |
| Moderate | _ | _% |
| Low | _ | _% |
| Total | _ | 100% |
By WCAG Principle
| Principle | Issues | Most Common |
|---|---|---|
| Perceivable | _ | [Top issue] |
| Operable | _ | [Top issue] |
| Understandable | _ | [Top issue] |
| Robust | _ | [Top issue] |
Automated Tool Score
| Tool | Score | Notes |
|---|---|---|
| Lighthouse Accessibility | _/100 | [Brief note] |
| axe DevTools | _ violations, _ passes | [Brief note] |
---
Findings
Finding 1: [Issue title]
Severity: Critical / High / Moderate / Low WCAG Criterion: [Number] — [Name] (Level [A/AA/AAA]) Principle: Perceivable / Operable / Understandable / Robust Location: [Page, component, or element where the issue occurs] Affects: [Screen reader users / Keyboard users / Low vision users / Cognitive disabilities / All users]
Description: [What the issue is and why it matters]
Steps to reproduce: 1. [Step] 2. [Step] 3. [Observe: description of the problem]
Current code:
[Code showing the issue]Recommended fix:
[Code showing the fix]Effort: Small / Medium / Large
---
Finding 2: [Issue title]
Severity: Critical / High / Moderate / Low WCAG Criterion: [Number] — [Name] (Level [A/AA/AAA]) Principle: Perceivable / Operable / Understandable / Robust Location: [Page, component, or element] Affects: [User groups]
Description: [What and why]
Steps to reproduce: 1. [Step]
Current code:
[Code]Recommended fix:
[Code]Effort: Small / Medium / Large
---
Finding 3: [Issue title]
[Same structure as above]
---
Prioritized Remediation Plan
| Priority | Finding | WCAG | Severity | Effort | Tier | Owner |
|---|---|---|---|---|---|---|
| 1 | [Finding title] | [Criterion] | Critical | Small | 1 | [Team] |
| 2 | [Finding title] | [Criterion] | High | Medium | 1 | [Team] |
| 3 | [Finding title] | [Criterion] | High | Small | 1 | [Team] |
| 4 | [Finding title] | [Criterion] | Moderate | Medium | 2 | [Team] |
| 5 | [Finding title] | [Criterion] | Low | Small | 2 | [Team] |
| 6 | [Finding title] | [Criterion] | Low | Large | 3 | [Team] |
Tier definitions:
- Tier 1: Fix before next release — Critical/High severity or Level A violations
- Tier 2: Fix within current quarter — Moderate severity or Level AA violations
- Tier 3: Add to backlog — Low severity or Level AAA aspirational improvements
---
What Passed
Highlight what's working well. This encourages teams and identifies patterns to replicate.
- [Positive finding 1 — e.g., "All form inputs have programmatic labels"]
- [Positive finding 2 — e.g., "Keyboard navigation order is logical throughout"]
- [Positive finding 3 — e.g., "Error messages are descriptive and linked to fields"]
- [Positive finding 4 — e.g., "Skip navigation is present and functional"]
---
Testing Details
Pages/Flows Tested
| # | Page/Flow | URL | Issues Found |
|---|---|---|---|
| 1 | [Page name] | [URL] | [#] |
| 2 | [Page name] | [URL] | [#] |
| 3 | [Page name] | [URL] | [#] |
Keyboard Testing Results
| Page | All elements reachable? | Focus visible? | Focus order logical? | No traps? | Notes |
|---|---|---|---|---|---|
| [Page] | Yes / No | Yes / No | Yes / No | Yes / No | [Notes] |
Screen Reader Testing Results
| Page | Headings logical? | Landmarks present? | Labels announced? | Errors announced? | Notes |
|---|---|---|---|---|---|
| [Page] | Yes / No | Yes / No | Yes / No | Yes / No | [Notes] |
---
Recommendations for Ongoing Compliance
- [ ] Add automated testing to CI/CD — Use axe-core in Playwright/Cypress/Jest to prevent regressions
- [ ] Include accessibility in design reviews — Check contrast, focus states, and heading hierarchy in Figma
- [ ] Include accessibility in code reviews — Use the Quick Audit Checklist (see SKILL.md)
- [ ] Test with screen reader monthly — Assign rotating responsibility
- [ ] Re-audit after fixes — Verify Tier 1 fixes before release
- [ ] Schedule full re-audit — [Recommended date, typically quarterly]
---
Appendix
Tools Used
- [Tool 1]: [Version, settings]
- [Tool 2]: [Version, settings]
Standards Reference
Accessibility Quick Checklist
Page/Component: [What you're checking] Date: [Date] Checker: [Name]
Use this during sprint reviews, PR reviews, or quick spot-checks. Each item maps to a WCAG criterion.
---
Structure and Semantics
- [ ] Page has
<html lang="...">— (3.1.1 A) - [ ] Page has one
<h1>, headings don't skip levels — (1.3.1 A) - [ ] Landmarks present:
<header>,<nav>,<main>,<footer>— (1.3.1 A) - [ ] Skip link present and functional — (2.4.1 A)
- [ ] Page has descriptive
<title>— (2.4.2 A) - [ ] Lists use
<ul>/<ol>, not styled<div>s — (1.3.1 A) - [ ] Data tables use
<th>withscope— (1.3.1 A)
Images
- [ ] Functional images have descriptive
alt— (1.1.1 A) - [ ] Decorative images have
alt=""— (1.1.1 A) - [ ] Complex images have extended description — (1.1.1 A)
Forms
- [ ] Every input has a
<label>— (1.3.1 A) - [ ] Required fields indicated (not by color alone) — (3.3.2 A)
- [ ] Errors identify the field + describe the fix — (3.3.1 A, 3.3.3 AA)
- [ ]
autocompleteon personal data fields — (1.3.5 AA) - [ ] No redundant entry in single-session flows — (3.3.7 A)
Keyboard
- [ ] All interactive elements reachable via Tab — (2.1.1 A)
- [ ] Focus indicator visible on all elements — (2.4.7 AA)
- [ ] Focus order matches visual order — (2.4.3 A)
- [ ] No keyboard traps — (2.1.2 A)
- [ ] Modals trap focus, close on Esc, return focus — (2.1.2 A, 4.1.2 A)
- [ ] Focused elements not hidden behind sticky content — (2.4.11 AA)
Color and Contrast
- [ ] Text contrast ≥ 4.5:1 (≥ 3:1 large text) — (1.4.3 AA)
- [ ] UI component contrast ≥ 3:1 — (1.4.11 AA)
- [ ] Color not sole method of conveying info — (1.4.1 A)
Interactive Elements
- [ ] Links have descriptive text (not "click here") — (2.4.4 A)
- [ ] Buttons have accessible names — (4.1.2 A)
- [ ] Touch targets ≥ 24×24px — (2.5.8 AA)
- [ ] Drag operations have non-drag alternatives — (2.5.7 AA)
- [ ] Custom components have correct ARIA — (4.1.2 A)
Dynamic Content
- [ ] Status messages announced via
aria-live— (4.1.3 AA) - [ ] Loading states communicated — (4.1.3 AA)
- [ ] Hover/focus content dismissible + hoverable — (1.4.13 AA)
Responsive and Visual
- [ ] Content readable at 200% zoom — (1.4.4 AA)
- [ ] No horizontal scroll at 320px width — (1.4.10 AA)
- [ ] Layout survives text spacing changes — (1.4.12 AA)
Media
- [ ] Videos have captions — (1.2.2 A)
- [ ] Audio-only has transcript — (1.2.1 A)
- [ ] Auto-playing media can be paused — (1.4.2 A)
- [ ] No content flashes > 3 times/second — (2.3.1 A)
Authentication (2.2)
- [ ] No CAPTCHAs or puzzles without alternatives — (3.3.8 AA)
- [ ] Help in consistent location across pages — (3.2.6 A)
---
Results
Issues found: [#] Critical/High: [List] Action items: [What needs to happen next]