DesignDeveloper skill

Web Accessibility Audit

Audits HTML/UI code against core accessibility and UX rules: semantics, ARIA, focus, contrast, keyboard paths, forms.

What it does

Plenty of interfaces work perfectly for someone with a mouse and good eyesight, and quietly do not work at all for anyone else. A menu built from the wrong kind of element cannot be reached by keyboard. A button with only an icon announces as nothing. Grey text on a white background is unreadable outdoors, and an error message that appears silently is invisible to someone using a screen reader.

This runs a structured pass over the interface in a deliberate order: the underlying elements first, then whether everything can be reached and used with a keyboard, whether it is visible where you are, whether every control has a name, then forms, contrast, zoom, motion and anything announced after the page loads. Each problem comes back with how serious it is, which rule it breaks, and the corrected code where the fix is short.

Nothing gets rewritten behind your back. You get a report, ordered worst first, plus a plain statement of what was and was not checked.

You end up with findings ranked worst first, each with the rule it breaks and the corrected markup.

When to reach for it

Use it before shipping anything people will use, after a redesign that moved markup or colors around, and whenever someone asks whether a page is accessible, works with a keyboard, or would pass an audit.

Skip it for work with no user interface, and know its limit: reviewing code finds many problems but cannot confirm focus order or announcements. Those need the page actually running, and the report says so when that was not possible.

What's inside

web-a11y-audit/
├── SKILL.md
└── references/
    ├── checklist.md
    └── examples.md
SKILL.md
---
name: web-a11y-audit
description: Audits interface code against WCAG 2.2 AA and core interaction rules - element semantics, keyboard operation, visible focus, accessible names, forms, contrast, motion and live regions - and reports each violation with a severity, the rule it breaks, and the corrected markup. Report-only. Use before shipping user-facing UI, or when asked whether something is accessible or works with a keyboard.
---

# Web Accessibility Audit

You find the failures that stop an interface working for people using a keyboard, a screen reader, magnification, or a phone in bright sun. You report them with severity and a fix; you do not silently rewrite the code, because the author needs to see what was broken and why.

The bar is WCAG 2.2 AA unless someone names a different one. (WCAG is the standard for web accessibility; AA is the level most organizations are held to, legally and contractually.)

## Workflow

Check in this order. Each stage assumes the one before it passed, because a fix at an early stage often deletes findings at a later one.

1. **Semantics.** Is the right element doing the job? `button` for actions, `a[href]` for navigation, real headings in order with one `h1`, lists as lists, `nav` / `main` / `header` / `footer` landmarks. A `div` with a click handler is the single most common failure and it causes several others downstream.
2. **Keyboard operation.** Everything interactive is reachable with Tab and Shift+Tab and operable with Enter, Space, and arrow keys where the pattern expects them. No positive `tabindex`. Focus is never trapped except deliberately, inside a modal dialog.
3. **Visible focus.** Every interactive element shows where focus is. `outline: none` with no replacement is a failure. Focus moves sensibly after an action: into a dialog when it opens, back to the trigger when it closes, to the error summary after a failed submit.
4. **Names and roles.** Every control has an accessible name. Icon-only buttons need one. Images need `alt`, empty `alt=""` when decorative. Do not add ARIA that repeats what the element already says.
5. **Forms.** Every field has a real associated label. Errors are tied to their field and announced. Required and invalid states are conveyed programmatically, not by color alone. Autocomplete is set on personal fields.
6. **Contrast and color.** Text at 4.5:1, large text and interface components at 3:1. Nothing carries meaning by color alone.
7. **Reflow and target size.** Usable at 200% zoom and at a 320px viewport with no horizontal scrolling or clipping. Interactive targets big enough to hit on a phone, with spacing between them.
8. **Motion and media.** `prefers-reduced-motion` respected. No motion that plays automatically without a way to stop it. Captions on video.
9. **Live regions.** Anything that appears asynchronously - a toast, a validation message, a loading state, a filtered result count - reaches assistive technology through a live region or a role that implies one.

```text
Audit pass checklist
[ ] Right element for the job, headings in order, landmarks present
[ ] Tab reaches every control, and arrow keys work where the pattern needs them
[ ] Focus is visible everywhere and moves correctly on open and close
[ ] Every control and image has an accessible name
[ ] Every field has a label; errors are tied to the field
[ ] Text contrast measured, not eyeballed
[ ] Works at 200% zoom and 320px wide
[ ] Reduced motion respected
[ ] Async updates are announced
```

## Report format

Report only. Do not rewrite the file. For each finding:

- **Severity**: Blocker, Serious, or Minor.
- **Location**: file and element.
- **Rule**: the principle it breaks, in one line.
- **Fix**: the concrete change, with corrected markup when it is short.

Group by severity, Blockers first, and end with a one-line verdict. If nothing is wrong in an area, say so rather than padding the report.

**Severity is decided by what the user cannot do:**

| Severity | Test | Examples |
| --- | --- | --- |
| Blocker | Someone cannot complete the task at all | Control unreachable by keyboard, no accessible name, focus trapped, form unusable without a mouse |
| Serious | Task is possible but the experience is degraded or error-prone | Contrast below the threshold, illogical focus order, errors not announced, headings out of order |
| Minor | Rough edge with a workaround | Redundant ARIA, a decorative image with descriptive alt text, a slightly small target |

## Known widget patterns

Custom widgets have expected keyboard and role behavior. Name the pattern rather than inventing markup, and check the real one against it:

| Widget | Expected |
| --- | --- |
| Modal dialog | `role="dialog"` `aria-modal="true"`, labelled, focus moves in on open, trapped while open, Escape closes, focus returns to the trigger |
| Tabs | Tab enters the tab list, arrow keys move between tabs, `aria-selected`, panel associated with its tab |
| Disclosure | A real `button` with `aria-expanded`, controlling the region it toggles |
| Combobox | Text input plus a listbox, arrows to move, Enter to choose, Escape to close, `aria-activedescendant` or managed focus |
| Menu button | `aria-haspopup`, `aria-expanded`, arrow keys within the menu, Escape closes and returns focus |
| Accordion | Each header is a button with `aria-expanded`, controlling its panel |

If a widget does not match its pattern, that is the finding. Do not invent a bespoke arrangement of ARIA to paper over it.

## How to verify quickly

- Tab from the top of the page to the bottom. Every stop should be visible, and nothing interactive should be skipped.
- Put the mouse away. Open the menu, fill the form, submit it, dismiss the dialog, using only keys.
- Zoom to 200%. Nothing clips, overlaps, or disappears.
- Narrow to 320px. Nothing scrolls sideways.
- Look at it in greyscale. Any meaning that vanishes was carried by color alone.
- Read only the headings, top to bottom. They should outline the page on their own.
- Read each link's text with the page covered. "Read more" tells you nothing about where it goes.

Automated checkers are worth running and will not finish the job: they reliably catch contrast, missing alt text and duplicate ids, and they cannot tell whether focus order makes sense or whether a name describes the control. The walkthrough above is what finds the rest.

## Example

**Finding, Blocker.** `components/SiteNav.jsx`, the menu items.

```jsx
<nav>
  <div className="link" onClick={() => go('/pricing')}>Pricing</div>
</nav>
```

**Rule.** Navigation must be real links. A `div` with a click handler is not focusable, is not announced as a link, cannot be opened in a new tab, and does not respond to Enter.

**Fix.** Use the element that already does this:

```jsx
<nav aria-label="Main">
  <a href="/pricing">Pricing</a>
</nav>
```

One change removes four separate findings: keyboard reachability, the accessible role, the missing name, and the focus ring, which the `a` element gets for free. This is why semantics is checked first.

## Rules

- Prefer the native element. The first rule of ARIA is not to use ARIA when HTML already does the job, and most ARIA findings are a symptom of the wrong element.
- Missing accessible name, unreachable control, or trapped focus is always a Blocker. Do not soften it because the fix is easy.
- Do not certify what you have not walked through. If you have not gone tab by tab, say the audit was static and name what it could not cover.
- Report the pattern by name for known widgets rather than improvising markup.
- A hint shown inside an empty field is not a label. It disappears the moment someone types.
- Do not pad the report. A short accurate audit is trusted; a long one full of hedged findings gets ignored.

## Edge cases

- **Only a screenshot is available.** You can assess contrast, target size, focus indication if visible, and color-only meaning. Say clearly that semantics, names and keyboard operation could not be checked.
- **A third-party widget is the problem.** Report it, name the version and the specific failure, and note whether it can be configured around. Do not patch a vendor's shadow DOM as a first move.
- **A design system component is at fault.** Report it once, at the component, and list the pages that inherit it rather than filing the same finding many times.
- **The framework generates the markup.** Audit the rendered output, not the source. The framework's abstraction is not what the browser hands to assistive technology.
- **Contrast fails only in one state**, such as a disabled control or a hover color. Report the state. Disabled controls have a lower bar but still need to be readable if their text matters.
- **An animation is the product**, such as a chart transition. Respect reduced motion by making the change instant, not by removing the feature.
- **Someone asks for a compliance statement.** Report findings and coverage. An audit finds failures; it cannot prove their absence.

## Files

- `references/checklist.md` - the full pass, area by area, with what to check, how to check it, and the severity each failure carries. Work through it during the audit.
- `references/examples.md` - three complete audits with their reports: a navigation and modal, a checkout form, and a custom select. Read to see how findings are worded and severities assigned.

Install it

Claude Code (this project)

Unzip the download into .claude/skills/. The folder is already named right.

Claude Code (every project)

Unzip into ~/.claude/skills/ to use it everywhere.

claude.ai

Upload the zip under Settings, then Capabilities, then Skills (paid plans).

How to call it

Just describe the task. The description line inside the skill does the matching. Or call it directly:

/web-a11y-audit run an accessibility check on this pagedoes this work with a keyboardis our site WCAG compliantfind the contrast and screen reader problemsreview this component for a11y before we ship

Works beyond Claude

Skills use the open Agent Skills format. The exact same folder works in OpenAI Codex, Gemini CLI, Cursor, GitHub Copilot and other tools that read the format.

Using a plain chat app? Paste SKILL.md into ChatGPT or Gemini as instructions. You lose auto-triggering, but the method still works.

agentskills.io

Pattern credit: Vercel — Web Interface Guidelines

Related skills