Accessibility Review
Perform a structured WCAG 2.2 AA accessibility audit of the specified component, page, or codebase. Execute all eight phases in order and produce an actionable, severity-ranked report.
Audit Scope
When invoked with a path, audit that file or directory. When invoked with "all" or no argument, locate the main source directory (src/, app/, components/, pages/) and audit the full codebase.
Exclude: node_modules, dist, build, .next, coverage, .astro.
Phase 1: Orient — Build the File Map
Before checking anything:
Use Glob to discover all UI files:
**/*.tsx, **/*.jsx, **/*.astro — components and pages
**/*.html — static markup
**/*.css, **/*.scss, **/*.module.css — stylesheets
**/tailwind.config.* — Tailwind configuration
Read package.json to detect the stack:
- React / Next.js / Astro / Vue / Svelte
- Radix UI / Headless UI / Tailwind CSS
- Testing libraries (jest-axe, @axe-core/playwright)
Count total component files. Report: "Scanning X components across Y directories."
Phase 2: Semantic Structure
Check every component file for semantic HTML violations.
Heading Hierarchy (WCAG 1.3.1)
- Flag any heading that skips a level (e.g.,
<h1> → <h3> with no <h2>)
- Flag multiple
<h1> elements on a single page
- Flag headings used purely for visual sizing (e.g.,
<h4> inside a card that is font-bold text-sm)
- Flag missing
<h1> on page-level components
Landmark Regions (WCAG 1.3.6)
- Flag pages missing
<main>, <header>, <nav>, or <footer> landmarks
- Flag multiple
<nav> or <header> elements that lack unique aria-label or aria-labelledby
- Flag
<div id="content"> patterns that should be <main>
Semantic Element Usage (WCAG 4.1.2)
- Flag
<div onClick> and <span onClick> without role and tabindex
- Flag
<div role="button"> when a <button> should be used
- Flag
<a> tags without href used as buttons
- Flag
<b> / <i> used for semantic emphasis (should be <strong> / <em>)
- Flag missing
<label> wrapping for icon-only inputs
- React: Flag wrapper
<div> inside <ul>, <ol>, <table> — suggest Fragments
Images and Alt Text (WCAG 1.1.1)
- Flag
<img> without alt attribute
- Flag
<img alt="image">, <img alt="photo">, or file-name-as-alt anti-patterns
- Flag
<img> with redundant alt (same as adjacent caption text)
- Flag decorative images without
alt="" or role="presentation"
Phase 3: ARIA Usage
Check all ARIA attribute usage across components.
The ARIA First Principles
- No ARIA is better than bad ARIA. Flag every instance where ARIA overrides correct native semantics.
- Flag
role="button" on <button> — redundant
- Flag
role="heading" on <h1>–<h6> — redundant
- Flag
role="list" on <ul> / <ol> — only needed in some browsers with Tailwind list-none
aria-hidden Misuse (WCAG 1.3.1, 4.1.2)
- Critical: Flag
aria-hidden="true" on any element that is focusable or contains focusable children
- Flag interactive buttons/links inside an
aria-hidden ancestor
- Flag SVG icons missing
aria-hidden="true" when decorative (or inside a labeled button)
Missing Accessible Names (WCAG 4.1.2, 2.4.6)
- Flag
<button> with no text content, no aria-label, and no aria-labelledby
- Flag icon-only buttons:
<button><svg>...</svg></button> with no accessible name
- Flag
<a> with no text, no aria-label, no aria-labelledby
- Flag
<input type="image"> without alt
ARIA State Synchronization (WCAG 4.1.2)
- Flag
aria-expanded attributes hardcoded to "true" or "false" that are not driven by state
- Flag disclosure components (accordions, dropdowns) missing
aria-expanded entirely
- Flag
aria-checked, aria-selected, aria-pressed not connected to component state
- React: Flag string literals
aria-expanded="true" — should be aria-expanded={isOpen}
Live Regions (WCAG 4.1.3)
- Flag dynamic status updates (toasts, alerts, loading states) with no
aria-live, role="status", or role="alert"
- Flag
role="alert" used for non-urgent messages (should be role="status")
- Flag live regions whose content is set before being added to the DOM (screen readers miss the announcement)
Phase 4: Keyboard and Focus
Check keyboard operability across all interactive components.
Focus Visibility (WCAG 2.4.7, 2.4.11)
- Critical: Flag any CSS with
outline: none, outline: 0, or Tailwind outline-none that is not immediately followed by a custom focus indicator
- Flag Tailwind
focus:outline-none without focus-visible:ring-* replacement
- Flag
*:focus { outline: none } global CSS reset without a replacement
Focus Order (WCAG 2.4.3)
- Flag
tabindex values greater than 0 (breaks natural DOM order)
- Flag visually ordered content whose DOM order is inverted via CSS
order, float, or position: absolute
Keyboard Traps (WCAG 2.1.2)
- Flag custom dropdown, popover, or menu components with no
Escape key handler
- Flag modal/dialog implementations that do not trap focus inside when open
- Flag custom components that intercept
Tab without providing a clear exit
Skip Links (WCAG 2.4.1)
- Flag page-level layouts that have no "Skip to main content" link
- Flag skip links present but permanently hidden (not just visually hidden until focused)
Interactive Element Requirements (WCAG 2.1.1)
- Flag
<div role="button"> / <div tabindex="0"> missing keydown/keyup handlers for Enter and Space
- Flag custom checkbox / radio implementations missing keyboard activation
React-Specific Focus Drift
- Flag
useEffect that removes a modal/panel from the DOM without calling .focus() on the trigger element
- Flag dialog close handlers that do not restore focus: look for
setIsOpen(false) or onClose() without a preceding triggerRef.current?.focus()
Phase 5: Color Contrast and Motion
Contrast (WCAG 1.4.3, 1.4.11)
Check Tailwind color utility classes and CSS color values for likely contrast failures:
- Flag text on colored backgrounds where the combination is known-low-contrast (e.g.,
text-gray-400 bg-white, text-yellow-300 bg-white)
- Flag UI component borders/outlines that use light neutrals (
border-gray-200, border-gray-300) on white backgrounds — likely fail 3:1
- Flag placeholder text (
::placeholder) styled with low-contrast colors
- Note: Full contrast ratios require a browser tool — flag suspects, recommend using axe DevTools or Colour Contrast Analyser for verification
Color-Only Meaning (WCAG 1.4.1)
- Flag error states indicated only by red border/text with no icon, label, or pattern
- Flag status indicators (badges, dots) that rely solely on color with no text or shape alternative
- Flag link text that is only differentiated from body text by color (no underline)
Motion and Animation (WCAG 2.3.3)
- Flag CSS
animation or transition on non-UI state changes (page decorations, hero animations) without @media (prefers-reduced-motion: reduce) override
- Flag Tailwind animations (
animate-spin, animate-bounce, animate-pulse) applied to non-status elements without motion-reduce: or motion-safe: modifier
- Flag auto-playing videos or carousels with no pause/stop control
- Flag content that flashes or blinks (potential seizure trigger)
Target Size (WCAG 2.5.8 — new in 2.2)
- Flag interactive targets smaller than 24×24px — look for
w-4 h-4, p-0, text-xs buttons that are icon-only
- Flag tightly packed link lists with less than 24px spacing between targets
Phase 6: Forms
Check all form components.
Label Association (WCAG 1.3.1, 3.3.2)
- Critical: Flag
<input>, <select>, <textarea> without an associated <label> (via for/id or wrapping)
- Flag
htmlFor in React that does not match an id on any input
- Flag
placeholder used as the only labeling mechanism
Required and Validation (WCAG 3.3.1, 3.3.2)
- Flag required fields with no
required attribute or aria-required="true"
- Flag required indicators (asterisks) with no explanation in the form instructions
- Flag error messages that are not programmatically linked to their input via
aria-describedby
- Flag
aria-invalid not set to "true" on invalid inputs during validation
Fieldset/Legend (WCAG 1.3.1)
- Flag groups of related checkboxes or radio buttons not wrapped in
<fieldset> + <legend>
- Flag
<legend> used but empty or containing only whitespace
Phase 7: SVG and Icons
Check all SVG usage.
Decorative SVGs (WCAG 1.1.1)
- Flag inline
<svg> without aria-hidden="true" when inside a labeled button or link (screen readers will attempt to read raw SVG content)
- Flag
<svg> without focusable="false" in IE/Edge-compatible codebases
Informative SVGs (WCAG 1.1.1)
- Flag standalone SVGs (logos, charts, illustrations) without
role="img" and either aria-label or a hidden text alternative
- Flag SVGs with a
<title> but no aria-labelledby pointing to it (cross-browser reliability issue)
Phase 8: Stack-Specific Checks
Tailwind CSS
- Flag
outline-none without focus-visible:ring-* replacement (most common Tailwind a11y failure)
- Flag missing
sr-only on icon-only button text labels
- Confirm
motion-safe: / motion-reduce: usage on animated elements
- Check for
list-none on <ul> — some screen readers strip list semantics; recommend role="list" if semantics matter
React
- Confirm
aria-* props use JSX booleans correctly: aria-expanded={isOpen} not aria-expanded="true"
- Flag
key props on focusable list items that change order (forces DOM remount, loses focus position)
- Flag
dangerouslySetInnerHTML in components — requires manual a11y verification of injected content
Astro
- Flag islands using
client:visible or client:idle on keyboard-interactive components — risk of hydration dead zones
- Recommend pre-hydrated ARIA states via
data-* attributes for components that render with client:visible
- Check for skip link in the root layout (
src/layouts/*.astro)
Radix UI / Headless UI
- Check that
asChild usage spreads all props and forwards refs — missing ref breaks Radix focus management
- Confirm Radix
Dialog.Content has aria-labelledby pointing to Dialog.Title
Reporting Format
Group findings into four severity levels. Report only findings with evidence.
Critical (blocks PR)
Issues that make the UI completely unusable for keyboard or screen reader users.
- Keyboard traps (no Escape, focus stuck)
- Missing focus indicators (bare
outline-none with no replacement)
aria-hidden="true" on focusable elements
- Interactive
<div> with no keyboard handler
Serious (fix before ship)
Issues that significantly impair assistive technology users.
- Missing form labels
- Icon-only buttons with no accessible name
- Missing heading structure on pages
- Dynamic content with no live region announcement
Moderate (fix in next sprint)
Issues that degrade the experience for some users.
- Skipped heading levels
- Missing skip links
- Hardcoded ARIA states not driven by component state
- Decorative SVGs not hidden from screen readers
Minor (best practice)
Low-impact issues or recommendations.
- Missing
lang on <html>
- Generic link text ("click here", "read more")
tabindex > 0 values
- Missing
aria-label on duplicate landmarks
For each finding, output:
[SEVERITY] WCAG X.X.X — Short description
File: path/to/file.tsx:line
Code: <the problematic snippet>
Fix: Specific remediation
End the report with:
- Total count by severity
- WCAG 2.2 AA compliance summary (Pass / Fail / Needs Manual Verification)
- Top 3 priority fixes
What Automated Tools Catch vs. Manual Review
Automated scanning (axe-core, Lighthouse) catches ~30–80% of issues. These require manual verification:
- Whether alt text is meaningful (not just present)
- Reading order matches visual order
- Focus indicator is sufficiently visible (not just technically present)
- Live region announcements are timed correctly
- Keyboard interactions feel logical to a user
Always note when a finding requires manual verification with a screen reader.
1---2name: a11y-review3description: This skill should be used when the user asks for an accessibility review, accessibility audit, a11y check, WCAG compliance review, screen reader audit, "is this accessible?", "does my site meet WCAG?", "check my component for accessibility", "accessibility overview of this page", "review accessibility of", "check for a11y issues", "fix accessibility issues", or any request to audit a web page, component, or codebase for WCAG 2.2 AA compliance.4---56# Accessibility Review78Perform a structured WCAG 2.2 AA accessibility audit of the specified component, page, or codebase. Execute all eight phases in order and produce an actionable, severity-ranked report.910## Audit Scope1112When invoked with a path, audit that file or directory. When invoked with "all" or no argument, locate the main source directory (src/, app/, components/, pages/) and audit the full codebase.1314Exclude: `node_modules`, `dist`, `build`, `.next`, `coverage`, `.astro`.1516---1718## Phase 1: Orient — Build the File Map1920Before checking anything:21221. Use Glob to discover all UI files:23 - `**/*.tsx`, `**/*.jsx`, `**/*.astro` — components and pages24 - `**/*.html` — static markup25 - `**/*.css`, `**/*.scss`, `**/*.module.css` — stylesheets26 - `**/tailwind.config.*` — Tailwind configuration27282. Read `package.json` to detect the stack:29 - React / Next.js / Astro / Vue / Svelte30 - Radix UI / Headless UI / Tailwind CSS31 - Testing libraries (jest-axe, @axe-core/playwright)32333. Count total component files. Report: "Scanning X components across Y directories."3435---3637## Phase 2: Semantic Structure3839Check every component file for semantic HTML violations.4041### Heading Hierarchy (WCAG 1.3.1)42- Flag any heading that skips a level (e.g., `<h1>` → `<h3>` with no `<h2>`)43- Flag multiple `<h1>` elements on a single page44- Flag headings used purely for visual sizing (e.g., `<h4>` inside a card that is `font-bold text-sm`)45- Flag missing `<h1>` on page-level components4647### Landmark Regions (WCAG 1.3.6)48- Flag pages missing `<main>`, `<header>`, `<nav>`, or `<footer>` landmarks49- Flag multiple `<nav>` or `<header>` elements that lack unique `aria-label` or `aria-labelledby`50- Flag `<div id="content">` patterns that should be `<main>`5152### Semantic Element Usage (WCAG 4.1.2)53- Flag `<div onClick>` and `<span onClick>` without `role` and `tabindex`54- Flag `<div role="button">` when a `<button>` should be used55- Flag `<a>` tags without `href` used as buttons56- Flag `<b>` / `<i>` used for semantic emphasis (should be `<strong>` / `<em>`)57- Flag missing `<label>` wrapping for icon-only inputs58- React: Flag wrapper `<div>` inside `<ul>`, `<ol>`, `<table>` — suggest Fragments5960### Images and Alt Text (WCAG 1.1.1)61- Flag `<img>` without `alt` attribute62- Flag `<img alt="image">`, `<img alt="photo">`, or file-name-as-alt anti-patterns63- Flag `<img>` with redundant alt (same as adjacent caption text)64- Flag decorative images without `alt=""` or `role="presentation"`6566---6768## Phase 3: ARIA Usage6970Check all ARIA attribute usage across components.7172### The ARIA First Principles73- **No ARIA is better than bad ARIA.** Flag every instance where ARIA overrides correct native semantics.74- Flag `role="button"` on `<button>` — redundant75- Flag `role="heading"` on `<h1>`–`<h6>` — redundant76- Flag `role="list"` on `<ul>` / `<ol>` — only needed in some browsers with Tailwind `list-none`7778### aria-hidden Misuse (WCAG 1.3.1, 4.1.2)79- **Critical:** Flag `aria-hidden="true"` on any element that is focusable or contains focusable children80- Flag interactive buttons/links inside an `aria-hidden` ancestor81- Flag SVG icons missing `aria-hidden="true"` when decorative (or inside a labeled button)8283### Missing Accessible Names (WCAG 4.1.2, 2.4.6)84- Flag `<button>` with no text content, no `aria-label`, and no `aria-labelledby`85- Flag icon-only buttons: `<button><svg>...</svg></button>` with no accessible name86- Flag `<a>` with no text, no `aria-label`, no `aria-labelledby`87- Flag `<input type="image">` without `alt`8889### ARIA State Synchronization (WCAG 4.1.2)90- Flag `aria-expanded` attributes hardcoded to `"true"` or `"false"` that are not driven by state91- Flag disclosure components (accordions, dropdowns) missing `aria-expanded` entirely92- Flag `aria-checked`, `aria-selected`, `aria-pressed` not connected to component state93- React: Flag string literals `aria-expanded="true"` — should be `aria-expanded={isOpen}`9495### Live Regions (WCAG 4.1.3)96- Flag dynamic status updates (toasts, alerts, loading states) with no `aria-live`, `role="status"`, or `role="alert"`97- Flag `role="alert"` used for non-urgent messages (should be `role="status"`)98- Flag live regions whose content is set before being added to the DOM (screen readers miss the announcement)99100---101102## Phase 4: Keyboard and Focus103104Check keyboard operability across all interactive components.105106### Focus Visibility (WCAG 2.4.7, 2.4.11)107- **Critical:** Flag any CSS with `outline: none`, `outline: 0`, or Tailwind `outline-none` that is not immediately followed by a custom focus indicator108- Flag Tailwind `focus:outline-none` without `focus-visible:ring-*` replacement109- Flag `*:focus { outline: none }` global CSS reset without a replacement110111### Focus Order (WCAG 2.4.3)112- Flag `tabindex` values greater than 0 (breaks natural DOM order)113- Flag visually ordered content whose DOM order is inverted via CSS `order`, `float`, or `position: absolute`114115### Keyboard Traps (WCAG 2.1.2)116- Flag custom dropdown, popover, or menu components with no `Escape` key handler117- Flag modal/dialog implementations that do not trap focus inside when open118- Flag custom components that intercept `Tab` without providing a clear exit119120### Skip Links (WCAG 2.4.1)121- Flag page-level layouts that have no "Skip to main content" link122- Flag skip links present but permanently hidden (not just visually hidden until focused)123124### Interactive Element Requirements (WCAG 2.1.1)125- Flag `<div role="button">` / `<div tabindex="0">` missing `keydown`/`keyup` handlers for `Enter` and `Space`126- Flag custom checkbox / radio implementations missing keyboard activation127128### React-Specific Focus Drift129- Flag `useEffect` that removes a modal/panel from the DOM without calling `.focus()` on the trigger element130- Flag dialog close handlers that do not restore focus: look for `setIsOpen(false)` or `onClose()` without a preceding `triggerRef.current?.focus()`131132---133134## Phase 5: Color Contrast and Motion135136### Contrast (WCAG 1.4.3, 1.4.11)137Check Tailwind color utility classes and CSS color values for likely contrast failures:138- Flag text on colored backgrounds where the combination is known-low-contrast (e.g., `text-gray-400 bg-white`, `text-yellow-300 bg-white`)139- Flag UI component borders/outlines that use light neutrals (`border-gray-200`, `border-gray-300`) on white backgrounds — likely fail 3:1140- Flag placeholder text (`::placeholder`) styled with low-contrast colors141- Note: Full contrast ratios require a browser tool — flag suspects, recommend using axe DevTools or Colour Contrast Analyser for verification142143### Color-Only Meaning (WCAG 1.4.1)144- Flag error states indicated only by red border/text with no icon, label, or pattern145- Flag status indicators (badges, dots) that rely solely on color with no text or shape alternative146- Flag link text that is only differentiated from body text by color (no underline)147148### Motion and Animation (WCAG 2.3.3)149- Flag CSS `animation` or `transition` on non-UI state changes (page decorations, hero animations) without `@media (prefers-reduced-motion: reduce)` override150- Flag Tailwind animations (`animate-spin`, `animate-bounce`, `animate-pulse`) applied to non-status elements without `motion-reduce:` or `motion-safe:` modifier151- Flag auto-playing videos or carousels with no pause/stop control152- Flag content that flashes or blinks (potential seizure trigger)153154### Target Size (WCAG 2.5.8 — new in 2.2)155- Flag interactive targets smaller than 24×24px — look for `w-4 h-4`, `p-0`, `text-xs` buttons that are icon-only156- Flag tightly packed link lists with less than 24px spacing between targets157158---159160## Phase 6: Forms161162Check all form components.163164### Label Association (WCAG 1.3.1, 3.3.2)165- **Critical:** Flag `<input>`, `<select>`, `<textarea>` without an associated `<label>` (via `for`/`id` or wrapping)166- Flag `htmlFor` in React that does not match an `id` on any input167- Flag `placeholder` used as the only labeling mechanism168169### Required and Validation (WCAG 3.3.1, 3.3.2)170- Flag required fields with no `required` attribute or `aria-required="true"`171- Flag required indicators (asterisks) with no explanation in the form instructions172- Flag error messages that are not programmatically linked to their input via `aria-describedby`173- Flag `aria-invalid` not set to `"true"` on invalid inputs during validation174175### Fieldset/Legend (WCAG 1.3.1)176- Flag groups of related checkboxes or radio buttons not wrapped in `<fieldset>` + `<legend>`177- Flag `<legend>` used but empty or containing only whitespace178179---180181## Phase 7: SVG and Icons182183Check all SVG usage.184185### Decorative SVGs (WCAG 1.1.1)186- Flag inline `<svg>` without `aria-hidden="true"` when inside a labeled button or link (screen readers will attempt to read raw SVG content)187- Flag `<svg>` without `focusable="false"` in IE/Edge-compatible codebases188189### Informative SVGs (WCAG 1.1.1)190- Flag standalone SVGs (logos, charts, illustrations) without `role="img"` and either `aria-label` or a hidden text alternative191- Flag SVGs with a `<title>` but no `aria-labelledby` pointing to it (cross-browser reliability issue)192193---194195## Phase 8: Stack-Specific Checks196197### Tailwind CSS198- Flag `outline-none` without `focus-visible:ring-*` replacement (most common Tailwind a11y failure)199- Flag missing `sr-only` on icon-only button text labels200- Confirm `motion-safe:` / `motion-reduce:` usage on animated elements201- Check for `list-none` on `<ul>` — some screen readers strip list semantics; recommend `role="list"` if semantics matter202203### React204- Confirm `aria-*` props use JSX booleans correctly: `aria-expanded={isOpen}` not `aria-expanded="true"`205- Flag `key` props on focusable list items that change order (forces DOM remount, loses focus position)206- Flag `dangerouslySetInnerHTML` in components — requires manual a11y verification of injected content207208### Astro209- Flag islands using `client:visible` or `client:idle` on keyboard-interactive components — risk of hydration dead zones210- Recommend pre-hydrated ARIA states via `data-*` attributes for components that render with `client:visible`211- Check for skip link in the root layout (`src/layouts/*.astro`)212213### Radix UI / Headless UI214- Check that `asChild` usage spreads all props and forwards refs — missing ref breaks Radix focus management215- Confirm Radix `Dialog.Content` has `aria-labelledby` pointing to `Dialog.Title`216217---218219## Reporting Format220221Group findings into four severity levels. Report only findings with evidence.222223### Critical (blocks PR)224Issues that make the UI completely unusable for keyboard or screen reader users.225- Keyboard traps (no Escape, focus stuck)226- Missing focus indicators (bare `outline-none` with no replacement)227- `aria-hidden="true"` on focusable elements228- Interactive `<div>` with no keyboard handler229230### Serious (fix before ship)231Issues that significantly impair assistive technology users.232- Missing form labels233- Icon-only buttons with no accessible name234- Missing heading structure on pages235- Dynamic content with no live region announcement236237### Moderate (fix in next sprint)238Issues that degrade the experience for some users.239- Skipped heading levels240- Missing skip links241- Hardcoded ARIA states not driven by component state242- Decorative SVGs not hidden from screen readers243244### Minor (best practice)245Low-impact issues or recommendations.246- Missing `lang` on `<html>`247- Generic link text ("click here", "read more")248- `tabindex > 0` values249- Missing `aria-label` on duplicate landmarks250251For each finding, output:252```253[SEVERITY] WCAG X.X.X — Short description254File: path/to/file.tsx:line255Code: <the problematic snippet>256Fix: Specific remediation257```258259End the report with:260- Total count by severity261- WCAG 2.2 AA compliance summary (Pass / Fail / Needs Manual Verification)262- Top 3 priority fixes263264---265266## What Automated Tools Catch vs. Manual Review267268Automated scanning (axe-core, Lighthouse) catches ~30–80% of issues. These require manual verification:269- Whether alt text is *meaningful* (not just present)270- Reading order matches visual order271- Focus indicator is *sufficiently visible* (not just technically present)272- Live region announcements are timed correctly273- Keyboard interactions feel logical to a user274275Always note when a finding requires manual verification with a screen reader.