Accessibility Audit
You review the application's accessibility — whether people with disabilities can use it. The standard is WCAG 2.2 Level AA; for products sold in the EU, the European Accessibility Act (EAA) makes this legally required from June 2025 for a broad category of products and services.
This skill follows the library-wide rules in docs/CONVENTIONS.md. Read that first.
Scope
Applies whenever the application has a user interface — web, mobile web, desktop Electron, native mobile if the codebase is visible. Does not apply to backend-only services or internal CLIs (but see release-readiness for operator tooling).
Inputs
From orchestrator: scope_tier, jurisdiction, stack_summary, gitnexus_indexed. Plus:
ui_framework: react | vue | angular | svelte | ember | plain-html | ios-native | android-native | flutter | other
target_wcag_level: A | AA | AAA (default AA)
eaa_in_scope: true | false — does the product fall under the EAA? (e-commerce, banking, transport tickets, e-books, computing hardware + OS, telecoms, AV media services, ATMs, etc.)
If not provided, ask.
Finding ID prefix
A11Y — see CONVENTIONS.md §4.
Tier thresholds
| Tier |
WCAG A |
WCAG AA |
EAA applicable |
Auto-testing |
Manual screen-reader testing |
| prototype |
advisory |
advisory |
advisory |
optional |
optional |
| team |
required |
required for user-facing flows |
required if applicable |
required in CI |
recommended |
| scalable |
required |
required everywhere |
required if applicable + audited |
required with zero-regression gate |
required per major release |
Review surface
1. Semantic HTML
Semantic elements carry accessibility for free. Misused <div>/<span> is the most common accessibility failure.
<button> for clickable actions, not <div onClick>. The former is keyboard-focusable, has proper role, fires on Enter/Space.
<a href> for navigation, not <button> or <div> with onClick={() => navigate(...)}.
- Landmark elements:
<header>, <nav>, <main>, <aside>, <footer> — one <main> per page.
- Heading hierarchy
<h1>–<h6> with no skips.
- Form structure:
<label for> tied to <input id>, <fieldset> + <legend> for grouped inputs.
- Lists:
<ul> / <ol> for lists, not series of <div>.
- Tables:
<table> with <thead>, <tbody>, <th scope> for data tables. Never for layout.
<dialog> for modal dialogs (or well-implemented ARIA dialog pattern), not just a styled <div>.
2. Keyboard navigation
Every interactive element must be reachable and operable by keyboard.
- Tab order follows visual order (no surprising jumps).
- All custom interactive components (
role="button", role="menuitem", etc.) handle keyboard — Enter/Space for activation, Arrow keys for navigation in composite widgets, Escape for dismissal.
- Focus visible — no
outline: none without a replacement focus style (WCAG 2.4.7 Focus Visible).
- No keyboard traps — can always Tab out of every component.
- Skip links — "Skip to main content" for repetitive navigation.
- Modal dialogs trap focus inside while open and restore focus on close.
- Drag-and-drop has a keyboard-accessible alternative (WCAG 2.5.7).
Grep for anti-patterns:
tabindex="-1" on things users need to interact with.
tabindex="99" etc. — positive tabindex disrupts natural order.
onClick on <div> / <span> without corresponding onKeyDown and role="button" + tabindex="0".
outline: none / outline: 0 without :focus-visible replacement.
3. ARIA usage
ARIA is a prosthesis, not an enhancement. Rule of ARIA: "No ARIA is better than bad ARIA."
- First rule: if you can use a native HTML element, use it — don't reach for ARIA.
aria-label / aria-labelledby for elements that need a label but can't have a visible one (icon buttons).
aria-describedby for supplementary info (e.g. form hints, error messages).
aria-live regions for dynamic updates — polite for most, assertive only for interruptions.
aria-hidden="true" only for decorative content; never on focusable elements (breaks the accessibility tree relationship).
aria-expanded, aria-selected, aria-checked states on composite widgets.
- Complex widgets follow the WAI-ARIA Authoring Practices patterns (combobox, tabs, accordion, menu, treeview).
Common anti-patterns:
aria-label repeating visible text verbatim (redundant announcement).
- Missing
role on custom widgets.
aria-hidden on the entire page / <body> while a modal is open (should only hide siblings).
- Applying
role="button" without full keyboard support.
4. Color and contrast
- WCAG 1.4.3 — text contrast ratio at least 4.5:1 (AA) or 7:1 (AAA) for normal text; 3:1 (AA) or 4.5:1 (AAA) for large text (≥18pt or 14pt bold).
- WCAG 1.4.11 — non-text UI component contrast at least 3:1 (borders of inputs, icons conveying meaning, focus indicators).
- Color not the only channel of information (WCAG 1.4.1) — don't rely on red/green alone for error/success. Pair with icon, text, shape.
- Dark-mode parity — contrast checked in both themes.
- Tools:
axe-core, Chrome DevTools contrast checker, Stark, WebAIM contrast checker.
5. Text and content
- Resizable to 200% (WCAG 1.4.4) without loss of functionality — avoid fixed pixel sizes on text / container dimensions that clip text.
- Reflow at 320 CSS pixels width (WCAG 1.4.10) — layouts must adapt, no horizontal scrolling for body content.
- Line height ≥ 1.5× font size; paragraph spacing ≥ 2× font size; letter spacing ≥ 0.12× font size; word spacing ≥ 0.16× font size — when user adjusts text spacing (WCAG 1.4.12).
- Plain language where possible; reading level disclosed for complex content.
6. Images, icons, media
- Alt text on images:
- Informative images: describe the information the image conveys.
- Decorative images:
alt="" or CSS background + role="presentation".
- Functional images (image-only buttons, links): describe the action, not the picture.
- Complex images (charts, diagrams): longer description available via
aria-describedby or adjacent text.
- Avoid
alt="image of ..." — screen readers already announce it's an image.
- Icons:
aria-label for standalone icon buttons; aria-hidden="true" on decorative icons accompanied by visible text.
- SVG:
<title> inside for accessible name, or aria-label on <svg> if appropriate.
- Video:
- Captions for all pre-recorded audio (WCAG 1.2.2).
- Audio description for visual information in video (WCAG 1.2.3 / 1.2.5).
- Transcripts for audio-only media.
- Controls are keyboard-operable.
- Autoplay muted or user-initiated; auto-playing audio longer than 3s has a mechanism to pause/mute (WCAG 1.4.2).
- Live media: captions (WCAG 1.2.4) for live audio at AA.
7. Forms
- Every input has a
<label> — not just placeholder text (disappears on input, low contrast).
- Required fields marked visually and programmatically (
aria-required="true" or required).
- Error messages:
- Announced to screen readers (
aria-live="polite" region or aria-invalid + aria-describedby pointing to the error).
- Associated with the failing input.
- Clear and actionable — "Please enter a valid email address" not "Invalid input".
- Preserved across submits — don't clear the field the user mistyped in.
autocomplete attributes for common fields (WCAG 1.3.5) — name, email, tel, street-address, etc.
- Input types appropriate —
type="email", type="tel", type="date" for better mobile keyboards + semantics.
- Error prevention for critical actions (WCAG 3.3.4) — confirm, review, or undo for legal / financial submissions.
- Focus moves to first error on submit failure.
8. Dynamic content
- Content changes announced via
aria-live, not requiring users to detect them.
- Loading states communicated (
aria-busy, loading indicators with accessible names).
- Toast notifications use
role="status" or role="alert" depending on severity, and don't auto-dismiss before slow readers can process them.
- Infinite scroll has a "load more" fallback or proper virtualization that doesn't break screen readers.
9. Motion and sensory
10. Time limits
- Session timeouts: user warned before, given time to extend (WCAG 2.2.1).
- Re-authenticate preserves user's in-progress work.
- Time-limited interactions (e.g. checkout with limited-duration cart) provide extension / disable option.
11. Navigation and orientation
- Page title unique and descriptive (WCAG 2.4.2).
- Multiple ways to reach a page (WCAG 2.4.5) — nav, search, sitemap.
- Focus order meaningful (WCAG 2.4.3).
- Link text descriptive out of context (WCAG 2.4.4) — "Read more about X" not "click here".
- Current location indicated in nav.
- Breadcrumbs for deep hierarchies.
- Language declared at document level (
<html lang="en">) and on lang-switching elements (<span lang="fr">bonjour</span>) — WCAG 3.1.1 / 3.1.2.
12. Mobile / touch
- Target size ≥ 24×24 CSS pixels (WCAG 2.5.8 AA in WCAG 2.2, ≥ 44×44 advisory).
- Tap targets with adequate spacing.
- No drag-only interactions without alternative (WCAG 2.5.7).
- Pointer cancelation (WCAG 2.5.2) — down-event alone shouldn't trigger critical actions; allow cancel on up-event.
- Orientation not locked (WCAG 1.3.4) — works in both portrait and landscape unless essential.
13. Authentication
WCAG 2.2 added 3.3.8 Accessible Authentication (Minimum) at AA:
- Don't require a cognitive function test (memorizing a string, transcribing from an image) unless there's an alternative (e.g. copy-paste allowed, password managers allowed, third-party auth).
- CAPTCHAs have an alternative form — or, where possible, replace with hCaptcha/Turnstile silent challenges.
14. Screen reader experience
Auto-testing catches maybe 30% of issues. Manual screen reader testing is required at team+ tier for user-facing flows.
- Tools: NVDA (Windows, free), JAWS (Windows, commercial), VoiceOver (macOS, iOS, built-in), TalkBack (Android, built-in), Narrator (Windows).
- Flows to test:
- Signup / login
- Core transaction (purchase, submit, send)
- Error paths
- Modal dialogs
- Dynamic content updates
- What to listen for:
- Announcements make sense out of visual context.
- No "unlabeled button" / "link".
- Reading order matches logical order.
- Focused element's state communicated (expanded/collapsed, selected, disabled).
15. Internationalization (i18n) accessibility
- Strings externalized, not baked into JSX/HTML as literals that block translation.
- RTL support (Arabic, Hebrew) —
dir="rtl", logical CSS properties (margin-inline-start not margin-left).
- Plurals / gender handled by the i18n library, not string concatenation.
- Date / number / currency formatting locale-aware.
- Icon and imagery culturally appropriate.
16. Testing tooling
- Automated in CI:
axe-core (via jest-axe, @axe-core/playwright, cypress-axe, pa11y).
Lighthouse accessibility score as a CI gate (target ≥ 95 AA).
eslint-plugin-jsx-a11y for React projects.
- Framework-specific:
@angular-eslint/eslint-plugin-template, Vue a11y eslint plugins.
- Manual at team+ tier:
- Keyboard-only pass per release.
- Screen reader pass per release.
- Zoom to 200% / 400% pass.
- User testing with disabled users at scalable tier.
17. Accessibility statement and feedback
- Accessibility statement published (required under EAA and many public-sector procurement rules). Describes: conformance level, known issues + remediation timeline, alternative access routes, contact for accessibility feedback, date of last review.
- Feedback channel monitored — users can report barriers.
Category enum (for findings)
semantic-html
keyboard
aria
contrast
text-scaling
media
forms
dynamic-content
motion
time-limit
navigation
mobile-touch
authentication
screen-reader
i18n
tooling
statement
Severity guidance
| Level |
Examples |
| critical |
Critical flow (signup, checkout, submit) impossible with keyboard or screen reader. No alt text on functional images in primary flows. Color the only channel for a critical distinction (e.g. error vs success) in a payment flow. |
| high |
Form errors not announced. Contrast failing AA on interactive elements. Focus indicator missing. Modal missing focus trap. |
| medium |
Alt text present but unhelpful. Headings not hierarchical. Language attribute missing. Autoplay carousel without pause. |
| low |
Minor ARIA redundancy. Decorative icons without aria-hidden. |
| info |
Observations or WCAG AAA items not targeted. |
Example findings
Example 1 — Keyboard-inaccessible card with click handler
- id: A11Y-003
severity: high
category: keyboard
title: "Product card uses div+onClick, unreachable by keyboard"
location: "src/components/ProductCard.tsx:24"
description: |
ProductCard wraps its content in a `<div that navigates
to the product detail page on click. The element has no tabindex, no
role, and no keyboard handler. Keyboard users cannot open product
details, which blocks the core browse-to-purchase flow. VoiceOver
reports the element as a group, so screen reader users also cannot
activate it.
evidence:
- |
// src/components/ProductCard.tsx:24
return (
<div className="card" => navigate(`/p/${slug}`)}>
<img src={imageUrl} alt="" />
<h3>{title}</h3>
<p>{price}</p>
</div>
);
remediation:
plan_mode: |
Replace the click-on-div with a semantic `<a href={"/p/" + slug}>`
wrapping the card content. Adjust CSS so the link doesn't inherit
default underline behavior but retains focus-visible styling. Also
fix the empty-alt image: provide product name alt, or keep empty if
the title beneath is sufficient (prefer the latter to avoid repeat).
edit_mode: |
Proposed diff replaces div+onClick with <a>; adjusts card CSS;
removes redundant onClick handler. Safe — purely structural change.
references:
- "WCAG 2.1.1 Keyboard"
- "WCAG 4.1.2 Name, Role, Value"
- "WAI-ARIA Authoring Practices — don't use div for navigation"
wcag_success_criterion: "2.1.1"
blocker_at_tier: [team, scalable]
Example 2 — Error messages not associated with inputs
- id: A11Y-008
severity: high
category: forms
title: "Signup form errors displayed visually but not announced to screen readers"
location: "src/components/SignupForm.tsx:55-80"
description: |
The signup form shows validation errors in a red box next to each field,
but the errors aren't associated with their inputs: no `aria-invalid`,
no `aria-describedby` link, no live region. Screen reader users submit,
hear nothing, and have no way to discover why submit failed. Testing
with VoiceOver confirms: focus stays on the Submit button, which is now
disabled, with no announcement.
evidence:
- |
// src/components/SignupForm.tsx:66
<input type="email" name="email" value={email} />
{errors.email && <span className="error">{errors.email}</span>}
remediation:
plan_mode: |
1. Add `aria-invalid={!!errors.email}` to each input.
2. Give each error `id="email-error"` and add
`aria-describedby="email-error"` to the input when error exists.
3. Add a top-of-form `<div role="alert">` that summarizes errors on
submit failure; move focus to this region or to the first
errored input.
4. Keep visual red styling — it still serves sighted users.
edit_mode: |
Proposed: refactor input component to accept error prop and wire
aria attributes automatically. Changes apply to all forms using
this component (11 files). Request confirmation because it changes
form behavior.
references:
- "WCAG 3.3.1 Error Identification"
- "WCAG 3.3.3 Error Suggestion"
- "WCAG 1.3.1 Info and Relationships"
- "WAI-ARIA form validation pattern"
wcag_success_criterion: "3.3.1"
blocker_at_tier: [team, scalable]
Example 3 — Insufficient color contrast on secondary buttons
- id: A11Y-012
severity: medium
category: contrast
title: "Secondary button text contrast 3.1:1 — below WCAG AA 4.5:1"
location: "src/styles/buttons.css:42"
description: |
The `.btn-secondary` class uses `color: #999` on a white background,
yielding a 2.85:1 contrast ratio. This fails WCAG 1.4.3 (4.5:1 required
for normal text at AA). Users with low vision or those in bright
environments cannot read secondary button labels. Secondary buttons
appear throughout the UI including in the checkout summary where
"Edit cart" is primarily styled this way.
evidence:
- |
/* src/styles/buttons.css:42 */
.btn-secondary {
color: #999;
background: #ffffff;
/* contrast 2.85:1 — fails WCAG AA */
}
remediation:
plan_mode: |
Darken secondary button text color to at least #595959 (contrast
4.55:1) or #525252 (contrast 5.1:1). Verify the new tone in
Figma / design system and confirm with design.
edit_mode: |
Proposed diff updates `--color-text-secondary` token to #595959 and
adds a contrast test to the CI visual-regression suite. Affects
secondary buttons, muted text, placeholder colors — request
confirmation because it's a design-token change.
references:
- "WCAG 1.4.3 Contrast (Minimum) — Level AA"
wcag_success_criterion: "1.4.3"
blocker_at_tier: [team, scalable]
Example 4 — EAA-covered product has no accessibility statement
- id: A11Y-018
severity: high
category: statement
title: "E-commerce product has no published accessibility statement"
location: "process-level"
description: |
The product is a B2C e-commerce service targeting EU consumers — within
the scope of the European Accessibility Act (Directive (EU) 2019/882,
Annex I §IV). The EAA enters into force 28 June 2025 and requires an
accessibility statement describing conformance level, known
non-conformities, alternative access routes, contact for feedback, and
review date. No such statement exists on the site or in the repo.
evidence:
- "No `/accessibility`, `/a11y-statement`, or equivalent route found."
- "`public/` contains legal, privacy, cookies, terms — no accessibility."
remediation:
plan_mode: |
1. Draft an accessibility statement. Use the EU model statement
(Commission Implementing Decision (EU) 2018/1523) as base — it's
adequate for EAA purposes too.
2. Include: conformance status (AA target), known issues list with
remediation timeline, alternatives for known issues, feedback
email and target response time, last reviewed date.
3. Publish at /accessibility and link from footer.
4. Set a review cadence (annually minimum) and add to release
checklist.
edit_mode: |
Scaffolds `docs/accessibility-statement.md` and a route. Statement
content requires legal / a11y lead review — do not auto-publish.
references:
- "Directive (EU) 2019/882 — European Accessibility Act"
- "EN 301 549 v3.2.1 — Accessibility requirements for ICT products and services"
- "Commission Implementing Decision (EU) 2018/1523 (model statement)"
related_findings: [COMP-019]
blocker_at_tier: [team, scalable]
Dimension summary template
## Accessibility Summary
WCAG target: <A | AA | AAA>
EAA in scope: <yes | no | unclear>
UI framework: <...>
Automated test results: <axe score | Lighthouse a11y score | pa11y count>
Manual test status: <keyboard pass | screen-reader pass | not done>
Findings: <N critical, N high, N medium, N low, N info>
WCAG SCs failing: <list>
Top 3 accessibility barriers:
1. ...
2. ...
3. ...
Not assessed: <list with reasons>
Edit-mode remediation guidance
Safe:
- Adding
alt to images (require user for content, use alt="" for decorative).
- Adding
aria-label to icon buttons.
- Adding
lang attribute to <html>.
- Adjusting focus indicator CSS.
- Adding
:focus-visible rules to restore focus when outline: none was removed.
- Adding ESLint a11y plugin to config.
- Adding axe / jest-axe / Playwright-axe to existing test suites.
Require confirmation:
- Replacing
<div onClick> with semantic element — may affect existing CSS / tests.
- Changing color tokens — design-system change.
- Refactoring forms to add aria associations — touches many components.
- Adding reduced-motion CSS — changes animation behavior.
- Publishing accessibility statement — legally meaningful, needs sign-off.
Skill-specific do-nots
- Do not treat axe / Lighthouse scores as sufficient evidence of accessibility. Automated tools catch about 30% of issues.
- Do not add ARIA where native HTML suffices — bad ARIA is worse than no ARIA.
- Do not rely on color alone for meaning.
- Do not use
tabindex values greater than 0.
- Do not hide elements from screen readers (
aria-hidden="true") while keeping them visible and focusable — it creates "phantom" focus.
- Do not accept "we'll fix it post-launch" for EAA-scope products after June 2025 — it's an enforcement risk.
- Do not confuse "WCAG compliant" with "accessible". Conformance is a floor.
1---2name: accessibility-audit3description: Reviews web and application accessibility against WCAG 2.2 AA and the European Accessibility Act (EAA, enforceable June 2025). Covers semantic HTML, ARIA usage, keyboard navigation, focus management, color contrast, screen-reader compatibility, form labels and errors, alternative text, motion and animation preferences, internationalization and localization quality, and testing tool integration (axe, pa11y, Lighthouse, Playwright a11y). Use when the user asks about "accessibility", "a11y", "WCAG", "screen reader", "EAA", "keyboard nav", invokes /accessibility-audit, or when the orchestrator delegates on user-facing applications. Stack-agnostic on the approach; framework-specific heuristics for React / Vue / Angular / Svelte / plain HTML.4license: Apache-2.05---67# Accessibility Audit89You review the application's accessibility — whether people with disabilities can use it. The standard is WCAG 2.2 Level AA; for products sold in the EU, the European Accessibility Act (EAA) makes this legally required from June 2025 for a broad category of products and services.1011This skill follows the library-wide rules in [`docs/CONVENTIONS.md`](../../docs/CONVENTIONS.md). Read that first.1213## Scope1415Applies whenever the application has a user interface — web, mobile web, desktop Electron, native mobile if the codebase is visible. Does not apply to backend-only services or internal CLIs (but see release-readiness for operator tooling).1617## Inputs1819From orchestrator: `scope_tier`, `jurisdiction`, `stack_summary`, `gitnexus_indexed`. Plus:2021- `ui_framework`: react | vue | angular | svelte | ember | plain-html | ios-native | android-native | flutter | other22- `target_wcag_level`: A | AA | AAA (default AA)23- `eaa_in_scope`: true | false — does the product fall under the EAA? (e-commerce, banking, transport tickets, e-books, computing hardware + OS, telecoms, AV media services, ATMs, etc.)2425If not provided, ask.2627## Finding ID prefix2829`A11Y` — see `CONVENTIONS.md` §4.3031## Tier thresholds3233| Tier | WCAG A | WCAG AA | EAA applicable | Auto-testing | Manual screen-reader testing |34|---|---|---|---|---|---|35| prototype | advisory | advisory | advisory | optional | optional |36| team | **required** | **required for user-facing flows** | **required if applicable** | **required** in CI | recommended |37| scalable | **required** | **required everywhere** | **required if applicable + audited** | **required** with zero-regression gate | **required** per major release |3839## Review surface4041### 1. Semantic HTML4243Semantic elements carry accessibility for free. Misused `<div>`/`<span>` is the most common accessibility failure.4445- `<button>` for clickable actions, not `<div onClick>`. The former is keyboard-focusable, has proper role, fires on Enter/Space.46- `<a href>` for navigation, not `<button>` or `<div>` with `onClick={() => navigate(...)}`.47- Landmark elements: `<header>`, `<nav>`, `<main>`, `<aside>`, `<footer>` — one `<main>` per page.48- Heading hierarchy `<h1>`–`<h6>` with no skips.49- Form structure: `<label for>` tied to `<input id>`, `<fieldset>` + `<legend>` for grouped inputs.50- Lists: `<ul>` / `<ol>` for lists, not series of `<div>`.51- Tables: `<table>` with `<thead>`, `<tbody>`, `<th scope>` for data tables. Never for layout.52- `<dialog>` for modal dialogs (or well-implemented ARIA dialog pattern), not just a styled `<div>`.5354### 2. Keyboard navigation5556Every interactive element must be reachable and operable by keyboard.5758- Tab order follows visual order (no surprising jumps).59- All custom interactive components (`role="button"`, `role="menuitem"`, etc.) handle keyboard — Enter/Space for activation, Arrow keys for navigation in composite widgets, Escape for dismissal.60- Focus visible — no `outline: none` without a replacement focus style (WCAG 2.4.7 Focus Visible).61- No keyboard traps — can always Tab out of every component.62- Skip links — "Skip to main content" for repetitive navigation.63- Modal dialogs trap focus inside while open and restore focus on close.64- Drag-and-drop has a keyboard-accessible alternative (WCAG 2.5.7).6566Grep for anti-patterns:67- `tabindex="-1"` on things users need to interact with.68- `tabindex="99"` etc. — positive tabindex disrupts natural order.69- `onClick` on `<div>` / `<span>` without corresponding `onKeyDown` and `role="button"` + `tabindex="0"`.70- `outline: none` / `outline: 0` without `:focus-visible` replacement.7172### 3. ARIA usage7374ARIA is a prosthesis, not an enhancement. Rule of ARIA: "No ARIA is better than bad ARIA."7576- **First rule**: if you can use a native HTML element, use it — don't reach for ARIA.77- `aria-label` / `aria-labelledby` for elements that need a label but can't have a visible one (icon buttons).78- `aria-describedby` for supplementary info (e.g. form hints, error messages).79- `aria-live` regions for dynamic updates — `polite` for most, `assertive` only for interruptions.80- `aria-hidden="true"` only for decorative content; never on focusable elements (breaks the accessibility tree relationship).81- `aria-expanded`, `aria-selected`, `aria-checked` states on composite widgets.82- Complex widgets follow the WAI-ARIA Authoring Practices patterns (combobox, tabs, accordion, menu, treeview).8384Common anti-patterns:85- `aria-label` repeating visible text verbatim (redundant announcement).86- Missing `role` on custom widgets.87- `aria-hidden` on the entire page / `<body>` while a modal is open (should only hide siblings).88- Applying `role="button"` without full keyboard support.8990### 4. Color and contrast9192- **WCAG 1.4.3** — text contrast ratio at least 4.5:1 (AA) or 7:1 (AAA) for normal text; 3:1 (AA) or 4.5:1 (AAA) for large text (≥18pt or 14pt bold).93- **WCAG 1.4.11** — non-text UI component contrast at least 3:1 (borders of inputs, icons conveying meaning, focus indicators).94- **Color not the only channel of information** (WCAG 1.4.1) — don't rely on red/green alone for error/success. Pair with icon, text, shape.95- Dark-mode parity — contrast checked in both themes.96- Tools: `axe-core`, Chrome DevTools contrast checker, Stark, WebAIM contrast checker.9798### 5. Text and content99100- Resizable to 200% (WCAG 1.4.4) without loss of functionality — avoid fixed pixel sizes on text / container dimensions that clip text.101- Reflow at 320 CSS pixels width (WCAG 1.4.10) — layouts must adapt, no horizontal scrolling for body content.102- Line height ≥ 1.5× font size; paragraph spacing ≥ 2× font size; letter spacing ≥ 0.12× font size; word spacing ≥ 0.16× font size — when user adjusts text spacing (WCAG 1.4.12).103- Plain language where possible; reading level disclosed for complex content.104105### 6. Images, icons, media106107- **Alt text** on images:108 - Informative images: describe the information the image conveys.109 - Decorative images: `alt=""` or CSS background + `role="presentation"`.110 - Functional images (image-only buttons, links): describe the action, not the picture.111 - Complex images (charts, diagrams): longer description available via `aria-describedby` or adjacent text.112 - Avoid `alt="image of ..."` — screen readers already announce it's an image.113- **Icons**: `aria-label` for standalone icon buttons; `aria-hidden="true"` on decorative icons accompanied by visible text.114- **SVG**: `<title>` inside for accessible name, or `aria-label` on `<svg>` if appropriate.115- **Video**:116 - Captions for all pre-recorded audio (WCAG 1.2.2).117 - Audio description for visual information in video (WCAG 1.2.3 / 1.2.5).118 - Transcripts for audio-only media.119 - Controls are keyboard-operable.120 - Autoplay muted or user-initiated; auto-playing audio longer than 3s has a mechanism to pause/mute (WCAG 1.4.2).121- **Live media**: captions (WCAG 1.2.4) for live audio at AA.122123### 7. Forms124125- Every input has a `<label>` — not just `placeholder` text (disappears on input, low contrast).126- Required fields marked visually and programmatically (`aria-required="true"` or `required`).127- Error messages:128 - Announced to screen readers (`aria-live="polite"` region or `aria-invalid` + `aria-describedby` pointing to the error).129 - Associated with the failing input.130 - Clear and actionable — "Please enter a valid email address" not "Invalid input".131 - Preserved across submits — don't clear the field the user mistyped in.132- `autocomplete` attributes for common fields (WCAG 1.3.5) — `name`, `email`, `tel`, `street-address`, etc.133- Input types appropriate — `type="email"`, `type="tel"`, `type="date"` for better mobile keyboards + semantics.134- Error prevention for critical actions (WCAG 3.3.4) — confirm, review, or undo for legal / financial submissions.135- Focus moves to first error on submit failure.136137### 8. Dynamic content138139- Content changes announced via `aria-live`, not requiring users to detect them.140- Loading states communicated (`aria-busy`, loading indicators with accessible names).141- Toast notifications use `role="status"` or `role="alert"` depending on severity, and don't auto-dismiss before slow readers can process them.142- Infinite scroll has a "load more" fallback or proper virtualization that doesn't break screen readers.143144### 9. Motion and sensory145146- **Prefers-reduced-motion** respected (WCAG 2.3.3 AAA, but broadly expected at AA level of polish):147 ```css148 @media (prefers-reduced-motion: reduce) { ... }149 ```150- No content flashes more than 3 times per second (WCAG 2.3.1) — photosensitive epilepsy trigger.151- Parallax / scroll-linked animations have a preference-based opt-out.152- Autoplay animations / carousels have pause/stop/hide controls (WCAG 2.2.2).153- No information conveyed by sound / motion alone.154155### 10. Time limits156157- Session timeouts: user warned before, given time to extend (WCAG 2.2.1).158- Re-authenticate preserves user's in-progress work.159- Time-limited interactions (e.g. checkout with limited-duration cart) provide extension / disable option.160161### 11. Navigation and orientation162163- Page title unique and descriptive (WCAG 2.4.2).164- Multiple ways to reach a page (WCAG 2.4.5) — nav, search, sitemap.165- Focus order meaningful (WCAG 2.4.3).166- Link text descriptive out of context (WCAG 2.4.4) — "Read more about X" not "click here".167- Current location indicated in nav.168- Breadcrumbs for deep hierarchies.169- Language declared at document level (`<html lang="en">`) and on lang-switching elements (`<span lang="fr">bonjour</span>`) — WCAG 3.1.1 / 3.1.2.170171### 12. Mobile / touch172173- Target size ≥ 24×24 CSS pixels (WCAG 2.5.8 AA in WCAG 2.2, ≥ 44×44 advisory).174- Tap targets with adequate spacing.175- No drag-only interactions without alternative (WCAG 2.5.7).176- Pointer cancelation (WCAG 2.5.2) — down-event alone shouldn't trigger critical actions; allow cancel on up-event.177- Orientation not locked (WCAG 1.3.4) — works in both portrait and landscape unless essential.178179### 13. Authentication180181WCAG 2.2 added 3.3.8 Accessible Authentication (Minimum) at AA:182183- Don't require a cognitive function test (memorizing a string, transcribing from an image) unless there's an alternative (e.g. copy-paste allowed, password managers allowed, third-party auth).184- CAPTCHAs have an alternative form — or, where possible, replace with hCaptcha/Turnstile silent challenges.185186### 14. Screen reader experience187188Auto-testing catches maybe 30% of issues. Manual screen reader testing is required at team+ tier for user-facing flows.189190- **Tools**: NVDA (Windows, free), JAWS (Windows, commercial), VoiceOver (macOS, iOS, built-in), TalkBack (Android, built-in), Narrator (Windows).191- **Flows to test**:192 - Signup / login193 - Core transaction (purchase, submit, send)194 - Error paths195 - Modal dialogs196 - Dynamic content updates197- **What to listen for**:198 - Announcements make sense out of visual context.199 - No "unlabeled button" / "link".200 - Reading order matches logical order.201 - Focused element's state communicated (expanded/collapsed, selected, disabled).202203### 15. Internationalization (i18n) accessibility204205- Strings externalized, not baked into JSX/HTML as literals that block translation.206- RTL support (Arabic, Hebrew) — `dir="rtl"`, logical CSS properties (`margin-inline-start` not `margin-left`).207- Plurals / gender handled by the i18n library, not string concatenation.208- Date / number / currency formatting locale-aware.209- Icon and imagery culturally appropriate.210211### 16. Testing tooling212213- **Automated** in CI:214 - `axe-core` (via `jest-axe`, `@axe-core/playwright`, `cypress-axe`, `pa11y`).215 - `Lighthouse` accessibility score as a CI gate (target ≥ 95 AA).216 - `eslint-plugin-jsx-a11y` for React projects.217 - Framework-specific: `@angular-eslint/eslint-plugin-template`, Vue `a11y` eslint plugins.218- **Manual** at team+ tier:219 - Keyboard-only pass per release.220 - Screen reader pass per release.221 - Zoom to 200% / 400% pass.222- **User testing** with disabled users at scalable tier.223224### 17. Accessibility statement and feedback225226- **Accessibility statement** published (required under EAA and many public-sector procurement rules). Describes: conformance level, known issues + remediation timeline, alternative access routes, contact for accessibility feedback, date of last review.227- **Feedback channel** monitored — users can report barriers.228229## Category enum (for findings)230231- `semantic-html`232- `keyboard`233- `aria`234- `contrast`235- `text-scaling`236- `media`237- `forms`238- `dynamic-content`239- `motion`240- `time-limit`241- `navigation`242- `mobile-touch`243- `authentication`244- `screen-reader`245- `i18n`246- `tooling`247- `statement`248249## Severity guidance250251| Level | Examples |252|---|---|253| critical | Critical flow (signup, checkout, submit) impossible with keyboard or screen reader. No alt text on functional images in primary flows. Color the only channel for a critical distinction (e.g. error vs success) in a payment flow. |254| high | Form errors not announced. Contrast failing AA on interactive elements. Focus indicator missing. Modal missing focus trap. |255| medium | Alt text present but unhelpful. Headings not hierarchical. Language attribute missing. Autoplay carousel without pause. |256| low | Minor ARIA redundancy. Decorative icons without `aria-hidden`. |257| info | Observations or WCAG AAA items not targeted. |258259## Example findings260261### Example 1 — Keyboard-inaccessible card with click handler262263```yaml264- id: A11Y-003265 severity: high266 category: keyboard267 title: "Product card uses div+onClick, unreachable by keyboard"268 location: "src/components/ProductCard.tsx:24"269 description: |270 ProductCard wraps its content in a `<div onClick={...}>` that navigates271 to the product detail page on click. The element has no tabindex, no272 role, and no keyboard handler. Keyboard users cannot open product273 details, which blocks the core browse-to-purchase flow. VoiceOver274 reports the element as a group, so screen reader users also cannot275 activate it.276 evidence:277 - |278 // src/components/ProductCard.tsx:24279 return (280 <div className="card" onClick={() => navigate(`/p/${slug}`)}>281 <img src={imageUrl} alt="" />282 <h3>{title}</h3>283 <p>{price}</p>284 </div>285 );286 remediation:287 plan_mode: |288 Replace the click-on-div with a semantic `<a href={"/p/" + slug}>`289 wrapping the card content. Adjust CSS so the link doesn't inherit290 default underline behavior but retains focus-visible styling. Also291 fix the empty-alt image: provide product name alt, or keep empty if292 the title beneath is sufficient (prefer the latter to avoid repeat).293 edit_mode: |294 Proposed diff replaces div+onClick with <a>; adjusts card CSS;295 removes redundant onClick handler. Safe — purely structural change.296 references:297 - "WCAG 2.1.1 Keyboard"298 - "WCAG 4.1.2 Name, Role, Value"299 - "WAI-ARIA Authoring Practices — don't use div for navigation"300 wcag_success_criterion: "2.1.1"301 blocker_at_tier: [team, scalable]302```303304### Example 2 — Error messages not associated with inputs305306```yaml307- id: A11Y-008308 severity: high309 category: forms310 title: "Signup form errors displayed visually but not announced to screen readers"311 location: "src/components/SignupForm.tsx:55-80"312 description: |313 The signup form shows validation errors in a red box next to each field,314 but the errors aren't associated with their inputs: no `aria-invalid`,315 no `aria-describedby` link, no live region. Screen reader users submit,316 hear nothing, and have no way to discover why submit failed. Testing317 with VoiceOver confirms: focus stays on the Submit button, which is now318 disabled, with no announcement.319 evidence:320 - |321 // src/components/SignupForm.tsx:66322 <input type="email" name="email" value={email} onChange={...} />323 {errors.email && <span className="error">{errors.email}</span>}324 remediation:325 plan_mode: |326 1. Add `aria-invalid={!!errors.email}` to each input.327 2. Give each error `id="email-error"` and add328 `aria-describedby="email-error"` to the input when error exists.329 3. Add a top-of-form `<div role="alert">` that summarizes errors on330 submit failure; move focus to this region or to the first331 errored input.332 4. Keep visual red styling — it still serves sighted users.333 edit_mode: |334 Proposed: refactor input component to accept error prop and wire335 aria attributes automatically. Changes apply to all forms using336 this component (11 files). Request confirmation because it changes337 form behavior.338 references:339 - "WCAG 3.3.1 Error Identification"340 - "WCAG 3.3.3 Error Suggestion"341 - "WCAG 1.3.1 Info and Relationships"342 - "WAI-ARIA form validation pattern"343 wcag_success_criterion: "3.3.1"344 blocker_at_tier: [team, scalable]345```346347### Example 3 — Insufficient color contrast on secondary buttons348349```yaml350- id: A11Y-012351 severity: medium352 category: contrast353 title: "Secondary button text contrast 3.1:1 — below WCAG AA 4.5:1"354 location: "src/styles/buttons.css:42"355 description: |356 The `.btn-secondary` class uses `color: #999` on a white background,357 yielding a 2.85:1 contrast ratio. This fails WCAG 1.4.3 (4.5:1 required358 for normal text at AA). Users with low vision or those in bright359 environments cannot read secondary button labels. Secondary buttons360 appear throughout the UI including in the checkout summary where361 "Edit cart" is primarily styled this way.362 evidence:363 - |364 /* src/styles/buttons.css:42 */365 .btn-secondary {366 color: #999;367 background: #ffffff;368 /* contrast 2.85:1 — fails WCAG AA */369 }370 remediation:371 plan_mode: |372 Darken secondary button text color to at least #595959 (contrast373 4.55:1) or #525252 (contrast 5.1:1). Verify the new tone in374 Figma / design system and confirm with design.375 edit_mode: |376 Proposed diff updates `--color-text-secondary` token to #595959 and377 adds a contrast test to the CI visual-regression suite. Affects378 secondary buttons, muted text, placeholder colors — request379 confirmation because it's a design-token change.380 references:381 - "WCAG 1.4.3 Contrast (Minimum) — Level AA"382 wcag_success_criterion: "1.4.3"383 blocker_at_tier: [team, scalable]384```385386### Example 4 — EAA-covered product has no accessibility statement387388```yaml389- id: A11Y-018390 severity: high391 category: statement392 title: "E-commerce product has no published accessibility statement"393 location: "process-level"394 description: |395 The product is a B2C e-commerce service targeting EU consumers — within396 the scope of the European Accessibility Act (Directive (EU) 2019/882,397 Annex I §IV). The EAA enters into force 28 June 2025 and requires an398 accessibility statement describing conformance level, known399 non-conformities, alternative access routes, contact for feedback, and400 review date. No such statement exists on the site or in the repo.401 evidence:402 - "No `/accessibility`, `/a11y-statement`, or equivalent route found."403 - "`public/` contains legal, privacy, cookies, terms — no accessibility."404 remediation:405 plan_mode: |406 1. Draft an accessibility statement. Use the EU model statement407 (Commission Implementing Decision (EU) 2018/1523) as base — it's408 adequate for EAA purposes too.409 2. Include: conformance status (AA target), known issues list with410 remediation timeline, alternatives for known issues, feedback411 email and target response time, last reviewed date.412 3. Publish at /accessibility and link from footer.413 4. Set a review cadence (annually minimum) and add to release414 checklist.415 edit_mode: |416 Scaffolds `docs/accessibility-statement.md` and a route. Statement417 content requires legal / a11y lead review — do not auto-publish.418 references:419 - "Directive (EU) 2019/882 — European Accessibility Act"420 - "EN 301 549 v3.2.1 — Accessibility requirements for ICT products and services"421 - "Commission Implementing Decision (EU) 2018/1523 (model statement)"422 related_findings: [COMP-019]423 blocker_at_tier: [team, scalable]424```425426## Dimension summary template427428```markdown429## Accessibility Summary430431WCAG target: <A | AA | AAA>432EAA in scope: <yes | no | unclear>433UI framework: <...>434435Automated test results: <axe score | Lighthouse a11y score | pa11y count>436Manual test status: <keyboard pass | screen-reader pass | not done>437438Findings: <N critical, N high, N medium, N low, N info>439WCAG SCs failing: <list>440Top 3 accessibility barriers:441 1. ...442 2. ...443 3. ...444445Not assessed: <list with reasons>446```447448## Edit-mode remediation guidance449450Safe:451- Adding `alt` to images (require user for content, use `alt=""` for decorative).452- Adding `aria-label` to icon buttons.453- Adding `lang` attribute to `<html>`.454- Adjusting focus indicator CSS.455- Adding `:focus-visible` rules to restore focus when `outline: none` was removed.456- Adding ESLint a11y plugin to config.457- Adding axe / jest-axe / Playwright-axe to existing test suites.458459Require confirmation:460- Replacing `<div onClick>` with semantic element — may affect existing CSS / tests.461- Changing color tokens — design-system change.462- Refactoring forms to add aria associations — touches many components.463- Adding reduced-motion CSS — changes animation behavior.464- Publishing accessibility statement — legally meaningful, needs sign-off.465466## Skill-specific do-nots467468- Do not treat axe / Lighthouse scores as sufficient evidence of accessibility. Automated tools catch about 30% of issues.469- Do not add ARIA where native HTML suffices — bad ARIA is worse than no ARIA.470- Do not rely on color alone for meaning.471- Do not use `tabindex` values greater than 0.472- Do not hide elements from screen readers (`aria-hidden="true"`) while keeping them visible and focusable — it creates "phantom" focus.473- Do not accept "we'll fix it post-launch" for EAA-scope products after June 2025 — it's an enforcement risk.474- Do not confuse "WCAG compliant" with "accessible". Conformance is a floor.