Skip to content
Back to skills

Accessibility Audit

ASecurity

Audit a component or page for accessibility violations against WCAG 2.1 AA criteria. Use when asked for an accessibility audit, a11y check, WCAG compliance check, or "is X accessible".

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 6, 2026
developmentjavascriptjavabashreactaws

Works with

  • cursor
  • cli

Security analysis

A100/100

Scanned November 6, 2026

npx -y skills add robritacca-dotcom/rift-ds --skill accessibility-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Accessibility Audit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Accessibility Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/robritacca-dotcom-accessibility-audit/badge)](https://www.skillsdirectory.com/skills/robritacca-dotcom-accessibility-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: accessibility-audit
description: Audit a component or page for accessibility violations against WCAG 2.1 AA criteria. Use when asked for an accessibility audit, a11y check, WCAG compliance check, or "is X accessible".
icon: accessibility
displayDescription: "Audits a component or page against WCAG 2.1 AA criteria. Checks semantic HTML, ARIA usage, keyboard navigation, focus styles, and colour contrast via both source code analysis and live screenshots. Reports file and line-level findings with WCAG criterion and severity."
invoke: ["accessibility audit","a11y check on [component/page]","check WCAG compliance","is [X] accessible"]
---

# accessibility-audit

Audit a component or page for accessibility violations against WCAG 2.1 AA criteria.

## When invoked

Use this skill when asked to check accessibility, run an a11y audit, or find WCAG issues — phrases like "accessibility audit", "a11y check on [component/page]", "check WCAG compliance", "is [X] accessible".

## What is already automated — read this before auditing anything

**Axe runs on every Storybook story and fails the build.** `.storybook/preview.ts` sets `a11y.test: 'error'`, so `npm run test` (and therefore CI and `npm run verify`) already enforces WCAG 2.1 AA across the whole library — and the same run executes any story `play` function, so behavior a story asserts (focus, dismissal, keyboard) is enforced too. Start by running it:

```bash
npm run test
```

If that is green, every violation axe can detect is already absent — and re-checking icon-only button names, label association, `role="dialog"` naming or ARIA parent/child relationships by hand is duplicated effort.

**This skill exists for the three things that gate does not cover:**

1. **Colour contrast — excluded from the axe gate entirely.** `color-contrast` is switched off in `.storybook/preview.ts` by a settled decision of the owner's. **Read that override's comment first**: it is the authoritative record of why, and the single place the details belong. Since the 2026-08-20 split of the action colour by mode the shipped pairs the rule used to fail all clear AA; the rule stays off because the action colour is a token consumers re-point to their own palettes, so the gate would be judging a value the system does not own. One slice of contrast is build-enforced at token level instead: `scripts/validate-theme-presets.mjs` holds every action fill against the label or icon components draw on it — resting, hover and pressed, in both themes, for the shipped tokens and every preset — and its `ACTION_PAIRINGS` table is the authoritative list of what it covers. Every other rendered pair (text and surfaces, status, charts, alpha composites over real content) is the manual auditor's job, and **the single highest-value thing to audit**: nothing else checks it. Never propose re-enabling the rule without asking the owner first.
2. **What axe cannot see.** Axe catches roughly a third of WCAG issues. It cannot tell whether alt text is *meaningful*, whether focus order makes sense, or whether a helper message *should* have been associated with its control. (Behavior a story's `play` function asserts *is* covered — `Dialog.stories.tsx` proves the modal focus trap, restore, and stacked Escape in CI — so the manual job is the overlays with no play coverage yet.) (Two such bugs once shipped undetected until a manual survey found them: Dropdown announced neither its helper text nor its error state. Both were since fixed by moving Dropdown onto `Field`, but it took the manual survey, not axe, to find them.)
3. **Website pages beyond the automated sample.** A page-level axe pass (`scripts/validate-website-a11y.mjs`, in `verify` and CI) now runs against the served site in both themes with the same rule set — but only over the route sample in `scripts/served-site.mjs`, at one desktop viewport. Full-site sweeps, mobile viewports, and any page outside that sample remain this skill's job.

Report a finding as **already-enforced** if `npm run test` or the page-level axe pass would have caught it; that tells the reader the gate is working rather than implying a gap.

## Instructions

1. **Determine scope.** Accept one of:
   - A component name (e.g. `Dropdown`) → audits `src/components/Dropdown/Dropdown.tsx` and its CSS (the chat widget's own components are the `.tsx` modules in `src/chat/`; read the folder rather than a list. Its question flow moves focus deliberately: into the first option when a set of questions holds the composer, and back to the composer once the set is answered or skipped; check both by keyboard)
   - A website page URL (e.g. `/components/button`) → audits the live rendered page
   - `all-components` → audits all components in `src/components/` and the chat widget in `src/chat/`

2. **Read the source files.** For each component in scope, read the `.tsx` and `.css` files before taking screenshots.

3. **Structural audit (from source code).** Most items here are already enforced by axe — spend your effort on the ones marked **[manual]**, which it cannot evaluate:

   **Semantic HTML & ARIA:**
   - Interactive elements use correct roles (`button`, `link`, `checkbox`, etc.) — never a `<div onClick>` without `role` and `tabIndex`
   - Icon-only `<button>` elements have `aria-label` describing their action
   - **[manual]** `<img>` elements have *meaningful* `alt` text — axe only checks that the attribute exists; decorative images use `alt=""`
   - Form inputs are associated with `<label>` via `htmlFor`/`id`, or have `aria-label`
   - Modals and dialogs use `role="dialog"` and `aria-modal="true"`, with `aria-labelledby` pointing to the title
   - Lists use `<ul>`/`<ol>` + `<li>`, not `<div>` stacks
   - Heading hierarchy is logical — no h3 before h2, no skipped levels

   **Keyboard Navigation:**
   - **[manual]** All interactive elements are reachable by Tab, in an order that makes sense — axe cannot judge order
   - Custom interactive components handle `onKeyDown` for Enter/Space (buttons) and arrow keys (any component with roving or list focus — radio groups, segmented controls, listboxes, tablists)
   - **[manual where no `play` function covers it]** Modal/dialog *actually* traps focus while open and restores it to the trigger on close — axe sees the attributes, not the behaviour. The modal overlays share one behavior implementation (design.md's Components intro owns the contract), so a real trap/restore defect there is a finding against all of them, not one. The site chat is modal only in some of its forms, and its floating card's move and size grips are pointer-only by a recorded decision: design.md's ChatWidget spec says which forms trap focus and why the grips are not a finding
   - Escape key closes dismissible overlays (any floating panel — tooltips, popovers, menus, dialogs, pickers), and **only the topmost**: a popover, menu or visible tooltip open inside a dialog closes before the dialog does, and a press on a dialog's backdrop with a menu open closes only the menu. Every overlay dismisses through the shared layer stack (`scripts/validate-overlay-listeners.mjs` keeps it so), and `Dialog.stories.tsx`'s nested-overlay stories assert the contract; a component handling Escape in its own keydown handler must mark it handled with `preventDefault()`, so check that on anything new
   - A literal English accessible name the consumer cannot replace is a finding: it cannot be localised. `scripts/validate-accessible-names.mjs` catches literal `aria-label`, `aria-roledescription` and `*Label` attributes, and `scripts/validate-visible-text.mjs` literal JSX text; **[manual]** check English built in string literals and arrays (a weekday list, a `${n} results` template), which neither can see

   **Focus Styles:**
   - Every interactive element has a `:focus-visible` rule in its CSS
   - Focus ring uses the action colour token (`--color-action-primary-bg`) — design.md's action-colour rules are the authoritative list of sanctioned uses. Flag any `outline: none` without a visible replacement

   **Motion** (axe evaluates none of this):
   - **[manual]** Anything that animates for more than five seconds, or loops indefinitely, can be paused, stopped, or hidden (WCAG 2.2.2). CSS motion satisfies this through the reduced-motion block in `tokens-motion.css`, which carries two mechanisms and whose own header comment owns the contract: token collapse for token-consuming CSS, and a universal `*` guard that also covers hardcoded and `ds-allow(motion)`-sanctioned literal timings (an infinite loop on an off-scale duration is still guarded). Read it there rather than assuming which half applies
   - **[manual]** Animation driven from JavaScript is **outside that guard** — a `requestAnimationFrame` loop cannot be seen by CSS, so each one has to check `prefers-reduced-motion` itself. Enumerate the JS-side checks with a grep for `prefers-reduced-motion` across `src/components/**/*.tsx`, `src/chat/**/*.{ts,tsx}` (the streaming reveal's check is in a `.ts` file) and `website/src` — the grep is the inventory, not any list written here (the site's ambient background, which renders a single static frame under the preference, is one illustration; more exist and new ones keep arriving) — and confirm each still honours the preference rather than assuming the token layer covers it. That grep has one known blind spot: the recharts-backed charts animate their marks in from JavaScript but never contain the string — their guard is recharts' own `isAnimationActive: 'auto'` default, and design.md's chart-set spec owns the contract (a literal `true` would override the guard). Run a second grep for `isAnimationActive` across `src/components/` — the one expected hit is the shared entrance hook in `src/components/Chart/useChartAnimation.ts`, which keeps recharts' `'auto'` guard while an entrance is due and sets `false` once it has played (design.md's chart-set spec sanctions it); a literal `true`, or a chart setting the flag without the hook, is a finding to check against that spec
   - **[manual]** Motion triggered by interaction (parallax, cursor-reactive effects) is disabled under reduced motion, or is not essential (WCAG 2.3.3)

4. **Visual audit (from screenshots).** Start the preview server and screenshot the target in both light and dark mode (follow the `visual-review` skill pattern). Check:
   - **Colour contrast (the priority — axe does not check it):** compute the ratio for every foreground/background pair actually rendered, not just body text. Flag anything below 4.5:1 for normal text or 3:1 for large text and UI components (WCAG 1.4.3 / 1.4.11). Note which token is used. The `color-contrast` axe rule is off for every pair (see item 1 under "What is already automated"); the action-fill pairings are the one slice the build gates, so cross-check a failing action pair against `ACTION_PAIRINGS` in `scripts/validate-theme-presets.mjs` — a pair it lists is already enforced (a hit there means the render is not using the token the table names, unless the cell is pinned in `SANCTIONED_AA_GAPS` in the same script, which is an owner-accepted gap to note rather than raise), and a label or icon on an action fill that it does not list is a gap in the table worth reporting.
   - **Text sizing:** No text visually below ~12px (WCAG 1.4.4)
   - **Focus visibility:** Confirm focus rings are clearly visible in both light and dark themes
   - **Forced colors (WCAG 1.4.11 under high contrast):** render the target with forced colors on (Playwright's `forcedColors: 'active'`, or the browser's emulation) and check that every state carried by a fill (checked, selected, on, a count) still stands apart from the page, that a floating panel keeps an edge once its shadow is dropped, and that focus rings still show. `src/tokens/tokens-forced-colors.css` re-points the state-bearing roles at system colours (design.md's Forced colors section owns which); the page-level check covers only the table bench (`FORCED_CHECKS` in `scripts/validate-website-a11y.mjs`), so anything else is this audit's to judge. A state that vanishes is a finding against the token layer, not the component, unless the component draws the state outside the tokens.

   Stop the server when done.

5. **For each issue, report:**

   ```
   src/components/FilterMenu/FilterMenu.tsx:84 — WCAG 4.1.2 Name, Role, Value [Critical]
   Trigger button has no accessible name. Icon-only button needs aria-label="Open filters".
   ```

   (The example is fictional by design — `FilterMenu` is not a real component; it illustrates the report shape only.)

   Severity:
   - **Critical** — blocks keyboard or screen reader users entirely
   - **Moderate** — degrades experience significantly
   - **Minor** — best practice violation, low direct impact

6. **Summarise:**
   - `X critical · Y moderate · Z minor`
   - If clean: "No accessibility violations found. Component meets WCAG 2.1 AA."

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in development →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.