Static analysis pass over JSX/HTML/CSS source for WCAG 2.2 AA failure patterns. Does NOT run axe-core, does NOT measure real contrast, does NOT test runtime behavior — pair with a11y-testing for that. Use when the user asks for an accessibility review, a11y check, WCAG review, "is this accessible?", "does my site meet WCAG?", "check my component for accessibility", "review accessibility of", "check for a11y issues", "fix accessibility issues", or any request to scan a web page, component, or ...
Scanned 5/27/2026
Install via CLI
openskills install radesjardins/RAD-Claude-Skills---
name: a11y-review
description: >
Static analysis pass over JSX/HTML/CSS source for WCAG 2.2 AA failure patterns. Does NOT run
axe-core, does NOT measure real contrast, does NOT test runtime behavior — pair with a11y-testing
for that. Use when the user asks for an accessibility review, a11y check, WCAG review, "is this
accessible?", "does my site meet WCAG?", "check my component for accessibility", "review
accessibility of", "check for a11y issues", "fix accessibility issues", or any request to scan
a web page, component, or codebase for WCAG 2.2 AA failure patterns. Findings are tagged by
detection confidence; the report does not produce a Pass/Fail verdict because static analysis
cannot defensibly produce one.
argument-hint: "[path/to/component, directory, or 'all' for full codebase scan]"
allowed-tools: Read, Glob, Grep, Bash
---
# Accessibility Review (static analysis)
Perform a structured **static analysis pass** over the specified component, page, or codebase. Execute all eight phases in order and produce an actionable, severity-ranked report with each finding tagged by confidence level.
## What this skill is — and what it isn't
This skill performs **pattern-based static analysis** over `.tsx` / `.jsx` / `.astro` / `.html` / `.css` source. It is **not** a WCAG audit, does **not** run axe-core, does **not** test runtime focus behavior, and does **not** test with screen readers. It does compute WCAG sRGB contrast ratios for Tailwind class pairs co-occurring on the same element (the `check-tailwind-contrast.py` validator), but it cannot see DOM-inherited backgrounds without runtime.
**What this skill catches well (high confidence):**
- Missing `alt` on `<img>`, missing `<label>` association, missing accessible name on icon buttons
- `outline: none` / `outline-none` without a focus-visible replacement (Tailwind users — axe often misses this because it inspects computed styles)
- `aria-hidden="true"` on focusable elements or their ancestors
- Hardcoded ARIA states (`aria-expanded="true"` as a string literal, not driven by component state)
- `<div onClick>` / `<span onClick>` without role + keyboard handlers
- Skipped heading levels, missing landmarks
- React-specific focus drift (modal close handlers without focus restoration)
- **Color contrast for Tailwind class pairs on the same element** — `check-tailwind-contrast.py` computes real WCAG 2.x sRGB ratios from the Tailwind v3 default palette plus custom colors parsed from `tailwind.config.{js,ts,cjs,mjs}`. Flags AA failures (4.5:1 body / 3:1 large or UI) and surfaces the actual computed ratio. Function-based palettes (colors imported from another file) are skipped with a warning.
**What this skill flags but cannot confirm (medium confidence):**
- Color contrast where the background comes from a DOM ancestor — the validator can't resolve inherited backgrounds across the rendered tree without a browser
- Color contrast involving alpha-blended (translucent) backgrounds — not handled by the validator
- ARIA state synchronization beyond the simplest cases
- Whether a `<fieldset>` correctly wraps a logical group
**What this skill cannot see at all (requires runtime / manual):**
- Whether `alt` text is *meaningful* — only checks presence and obvious anti-patterns
- Reading order matches visual order — needs DOM traversal at runtime
- Focus indicator is *sufficiently visible* — needs a browser
- Live region announcements arrive at the right time — needs real assistive tech
- Keyboard interactions feel logical to a user — needs a human
- Custom widget keyboard contracts implemented end-to-end — needs APG-pattern verification with a screen reader
- Screen reader announcement coherence — needs NVDA / JAWS / VoiceOver
For runtime verification, use `a11y-testing` to set up real axe via jest-axe and @axe-core/playwright. For manual verification, use a real screen reader.
## Audit Scope
When invoked with a path, scan that file or directory. When invoked with "all" or no argument, locate the main source directory (src/, app/, components/, pages/) and scan the full codebase.
Exclude: `node_modules`, `dist`, `build`, `.next`, `coverage`, `.astro`.
---
## Cross-model note
Works identically across Opus 4.7, Sonnet 4.6, and Haiku 4.5. The validators in Phase 0 are deterministic Python — model choice doesn't affect their output. Output schema is identical regardless of model.
**Execution differences by model:**
- **Opus 4.7 / Sonnet 4.6** — issue Phase 0 + Phase 1 reads + stack detection as a single parallel tool-call burst (see "Execution: parallel-first" below). The four validators run concurrently in one Bash call.
- **Haiku 4.5** — may follow phase order sequentially if parallel batching misbehaves. The four validators still run in parallel via the `&` + `wait` shell pattern; only the LLM-side reads fall back to sequential.
If any model loses tool-call coherence on the parallel batch, fall back to running Phase 0 first, then Phase 1 — final report is identical, only wall time changes.
---
## Execution: parallel-first
Phases 0 and 1 have no inter-phase dependencies — Phase 0 scripts scan files independently of Phase 1's file map, and Phase 1's file map is consumed only by Phases 2–8. Issue them as a single parallel batch:
**Batch to issue at the start of the skill:**
- **Bash (one command, scripts run concurrently)** — kick off all four validators with `&` and `wait`. They write JSON to `/tmp/rad-a11y-*.json`.
- **Glob in parallel** — discover `**/*.tsx`, `**/*.jsx`, `**/*.astro`, `**/*.html`, `**/*.css`, `**/*.scss`, `**/tailwind.config.*`.
- **Read in parallel** — `package.json` (stack detection), `tailwind.config.{js,ts,cjs,mjs}` if present, the four validator JSON outputs (after the Bash batch completes).
Phase 1's file count + stack detection is a synthesis over the Glob and Read results; it doesn't gate the validators or the LLM phases that follow.
Skip the parallel batch and run sequentially only if the model loses tool-call coherence — final report is identical regardless.
---
## Phase 0: Run validators (deterministic Python scripts)
**New in 2.1.** Before any LLM regex work, run all four rad-a11y validators in parallel. They emit JSON; their output populates the `[STATIC]` (and some `[HEURISTIC]`) findings deterministically. The LLM phases below then handle only what scripts can't decide — alt-text meaningfulness, complex ARIA logic, reading order, semantic intent — and tag those findings `[HEURISTIC]` or `[NEEDS-MANUAL]`.
**Skip Phase 0 silently if Python is unavailable** (`python3 --version` and `python --version` both fail). In that case, Phases 2–8 below run as the original LLM regex passes, with all findings tagged `[HEURISTIC]` since the deterministic backstop is missing. Note this in the report header: `⚠ Python unavailable — running in fallback mode; static patterns are heuristic.`
Execute as a single parallel Bash batch (one shell spawn for all four):
```bash
PY=$(command -v python3 || command -v python)
PR="${plugin_root}/scripts"
$PY "$PR/scan-jsx-patterns.py" "$PWD" > /tmp/rad-a11y-jsx.json &
$PY "$PR/check-tailwind-contrast.py" "$PWD" > /tmp/rad-a11y-contrast.json &
$PY "$PR/check-target-size.py" "$PWD" > /tmp/rad-a11y-target.json &
$PY "$PR/lint-aria.py" "$PWD" > /tmp/rad-a11y-aria.json &
wait
```
Then read all four JSON outputs in a parallel Read batch. Each emits the schema documented in `scripts/README.md`:
```json
{
"tool": "...",
"files_scanned": 42,
"findings_count": 7,
"findings": [ { "category": "...", "wcag": "...", "severity": "...", "confidence": "STATIC", "file": "...", "line": 12, "snippet": "...", "fix": "..." } ]
}
```
**Use the validator findings verbatim** — do not re-derive, paraphrase, or second-guess the snippet/line/category fields. They are deterministic; rewriting them adds drift. The LLM's job in Phases 2–8 is to add findings the scripts couldn't make, not to second-guess the ones they did.
If `lint-aria.py` reports `"plugin_installed": false`, surface its `recommendation` field once at the top of the report so the user knows to install `eslint-plugin-jsx-a11y` for higher-coverage linting.
---
## Phases 2–8: LLM-only patterns (everything scripts can't decide)
The phases below now run **after** Phase 0. They cover only what static scripts cannot determine — patterns requiring contextual judgment, cross-element analysis, or semantic intent. **Do not duplicate work the scripts already did**; if scan-jsx-patterns flagged a missing `alt`, do not re-flag it from the LLM pass. Instead, layer LLM judgment on:
- **Alt-text meaningfulness** — scripts catch missing/bad-pattern alt; LLM judges whether present alt is *informative*. Tag `[HEURISTIC]`.
- **ARIA logic in complex widgets** — scripts catch hardcoded literals; LLM judges whether ARIA states for tabs/menus/comboboxes follow the APG keyboard contract end-to-end. Tag `[HEURISTIC]`.
- **Reading order vs. visual order** — DOM-runtime concern; tag `[NEEDS-MANUAL]`.
- **Live region timing** — runtime concern; tag `[NEEDS-MANUAL]`.
- **Empty / icon-only buttons** — cross-element analysis (is there visible text in a sibling? a `sr-only` span?); tag `[HEURISTIC]`.
- **Placeholder-as-label** — cross-element (does a `<label htmlFor>` exist nearby?); tag `[HEURISTIC]`.
- **Fieldset grouping correctness** — is the group of related inputs actually wrapped? `[HEURISTIC]`.
Phases 1 (file map) and 2–8 below remain as written for the LLM-judgment slices. Phase 0 supersedes the deterministic-pattern slices that used to live inside Phases 2–8.
---
## Phase 1: Orient — Build the File Map (and detect stack)
In parallel with Phase 0 (see "Execution: parallel-first" above):
1. 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
2. Read `package.json` (if present) to detect the stack. Build a stack-detection record used by Phases 4 and 8 to skip irrelevant slices:
| Signal | Detected stack |
|---|---|
| `react` in dependencies | `react: true` |
| `next` in dependencies | `react: true, nextjs: true` |
| `astro` in dependencies | `astro: true` |
| `tailwindcss` in dependencies OR `tailwind.config.*` exists | `tailwind: true` |
| `@radix-ui/*` in dependencies | `radix: true` |
| `@headlessui/*` in dependencies | `headlessui: true` |
| None of the above + `.html` files exist | `plain_html: true` |
3. Count total component files. Report: "Scanning X components across Y directories. Detected stack: [list]."
### Stack-aware phase routing
Use the detection record to decide which downstream phases run:
- **Phase 4 (color/contrast/motion)** — runs always, but the contrast slice notes whether `check-tailwind-contrast.py` actually had Tailwind classes to evaluate. If `tailwind: false` AND no inline `text-*` / `bg-*` classes were found in source, the contrast phase reports "No Tailwind class pairs detected — runtime contrast verification required (use axe DevTools)" instead of running the LLM-judgment slice.
- **Phase 8 (stack-specific)** — only execute the slices matching detected stack:
- **Tailwind slice** — runs if `tailwind: true`
- **React slice** — runs if `react: true`
- **Astro slice** — runs if `astro: true`
- **Radix / Headless UI slice** — runs if `radix: true` or `headlessui: true`
- **Plain HTML** — only the framework-agnostic checks from Phases 2–7 apply; Phase 8 is skipped entirely with one line: "Phase 8 (stack-specific): no framework detected; framework-agnostic phases already covered."
Phases 2, 3, 5, 6, 7 (semantic, ARIA, contrast/motion, forms, SVG) are framework-agnostic and run regardless of stack.
---
## 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. **Every finding must carry a detection-confidence tag** in addition to its severity — readers should be able to see which findings the static scan can prove and which are pattern-based heuristics or hand-offs to manual verification.
### Detection-confidence tags
- **`[STATIC]`** — Deterministic detection: the pattern is present in source and the failure mode is unambiguous from source alone. Example: `<img>` with no `alt`, `aria-hidden="true"` on a `<button>`, `outline-none` with no `focus-visible:` replacement on the same element. These are PR-blockers when they're also Critical/Serious severity.
- **`[HEURISTIC]`** — LLM judgment over patterns where source isn't dispositive. Example: "this `aria-label` looks redundant with adjacent visible text," "this `<div role="button">` *probably* needs keyboard handlers." These should be reviewed before fixing — the model can be wrong.
- **`[NEEDS-MANUAL]`** — The pattern *suggests* a problem, but only a browser, axe runtime, or screen reader can confirm. Example: contrast pair flagged by class name (no real ratio computed), live region timing, focus indicator visibility quality, alt-text meaningfulness. Surfaced so the reader knows what to verify next.
### Severity levels (orthogonal to confidence)
#### Critical (blocks PR when `[STATIC]`)
Issues that make the UI completely unusable for keyboard or screen reader users.
- Keyboard traps (no Escape, focus stuck) — `[HEURISTIC]` from source; needs runtime confirmation
- Missing focus indicators (bare `outline-none` with no replacement) — `[STATIC]`
- `aria-hidden="true"` on focusable elements — `[STATIC]`
- Interactive `<div>` with no keyboard handler — `[STATIC]`
#### Serious (fix before ship)
Issues that significantly impair assistive technology users.
- Missing form labels — `[STATIC]` (presence) / `[HEURISTIC]` (correct association)
- Icon-only buttons with no accessible name — `[STATIC]`
- Missing heading structure on pages — `[STATIC]`
- Dynamic content with no live region announcement — `[STATIC]` (missing markup) / `[NEEDS-MANUAL]` (timing)
#### Moderate (fix in next sprint)
Issues that degrade the experience for some users.
- Skipped heading levels — `[STATIC]`
- Missing skip links — `[STATIC]`
- Hardcoded ARIA states not driven by component state — `[STATIC]`
- Decorative SVGs not hidden from screen readers — `[STATIC]`
#### Minor (best practice)
Low-impact issues or recommendations.
- Missing `lang` on `<html>` — `[STATIC]`
- Generic link text ("click here", "read more") — `[HEURISTIC]`
- `tabindex > 0` values — `[STATIC]`
- Missing `aria-label` on duplicate landmarks — `[STATIC]`
For each finding, output:
```
[SEVERITY] [CONFIDENCE] WCAG X.X.X — Short description
File: path/to/file.tsx:line
Code: <the problematic snippet>
Fix: Specific remediation
[Notes: <any caveat — e.g., "real contrast ratio not computed; verify with axe DevTools">]
```
### End-of-report summary
Do **not** issue a Pass / Fail / Compliance verdict. Static analysis cannot produce a defensible WCAG 2.2 AA pass/fail — that requires runtime + manual + assistive-tech verification. Instead, end with:
```
Static scan summary
-------------------
[STATIC] findings: <N> Critical, <N> Serious, <N> Moderate, <N> Minor
[HEURISTIC] findings: <N> total — review before fixing, model can be wrong
[NEEDS-MANUAL] flags: <N> total — verify with axe DevTools / screen reader
Top 3 priority fixes (highest severity × highest confidence):
1. <file:line> — <description>
2. <file:line> — <description>
3. <file:line> — <description>
Recommended next steps:
- Set up real axe runtime (use the a11y-testing skill) — covers categories static scan can't
- Manual screen reader pass on <list any [NEEDS-MANUAL] flagged components>
- Browser-based contrast verification on <list any color-related [NEEDS-MANUAL] flags>
```
---
## What real axe / Lighthouse catches that this skill doesn't, and vice versa
Real axe (running in a browser via jest-axe or @axe-core/playwright) catches a different subset than this static skill. They overlap heavily but each catches things the other misses.
**Real axe catches but this static skill cannot:**
- Computed contrast ratios (real sRGB → WCAG math against rendered colors)
- Duplicate `id` after dynamic component rendering
- ARIA attribute validity against the resolved role
- Some keyboard accessibility paths that depend on runtime DOM
**This static skill catches but real axe often misses:**
- Tailwind `outline-none` written without a `focus-visible:ring-*` replacement (axe sees the resolved `:focus` style, which may have a default UA outline depending on browser/version, leading to false negatives)
- Hardcoded ARIA state literals in JSX source (`aria-expanded="true"` as a string instead of `={isOpen}`) — axe sees the value at the moment of scan, missing the bug that the value never updates
- React modal close handlers that don't restore focus to the trigger (axe sees the focus state after close, but the underlying source pattern is invisible to it)
- Astro `client:visible` / `client:idle` on keyboard-interactive components (hydration timing issues)
The honest framing: **`/a11y-review` and real axe are complementary, not redundant.** Run both. The widely-cited "axe catches 30–80%" range refers to real axe — `/a11y-review` covers a subset of that range plus some patterns axe misses, which is why every finding here is tagged with detection confidence so you know what you actually have.
No comments yet. Be the first to comment!