Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .claude/skills/a11y-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
name: a11y-audit
description: >
On-demand accessibility audit using a multi-phase Red Team / Blue Team pipeline.
Load when asked to audit a component, page, or user flow for accessibility.
Combines persona-based functional simulation, semantic code audit, axe output
integration, and severity-weighted synthesis into a structured report.
---

# Accessibility Audit Skill

Run a three-phase audit and synthesize the findings into a risk-weighted report.

---

## Phase 1 — Red Team: Persona Simulation (Functional Friction)

Ignore code mechanics. Focus on **task success, cognitive load, and human-centred design**
for each of the six functional personas below. Flag issues that automated scanners miss.
Do not report things an axe scan would catch — those belong in Phase 3.

### 1. Screen Reader Navigator (Non-Visual)

Navigates linearly via audio output. Flag:
- Vague or duplicate link text ("read more", "view" repeated on every card)
- Missing landmark structure or unlabelled landmarks
- Dynamic content updates (filters, route changes) not announced
- Heading order that skips levels or misrepresents the page hierarchy
- Missing `aria-current="page"` on active nav links

### 2. Power Keyboard User (Motor Limit)

Uses only Tab, Shift+Tab, Enter, Space, and arrow keys. Flag:
- Focus traps with no Escape route
- Tab order that does not match visual reading order
- Interactive elements not reachable by Tab (custom elements without `tabindex`)
- Focus lost after dynamic UI changes (modal close, filter apply, route change)
- Missing skip link or skip link with no visible focus state

### 3. Magnification Expert (Low Vision)

Navigates with viewport zoomed to 200–400%. Flag:
- Horizontal scrolling required at 400% (reflow failure — WCAG 1.4.10)
- Sticky/fixed elements that consume most of the viewport at high zoom
- Text truncated with `overflow: hidden` that cannot be reached
- Focus indicator too small to see when zoomed (less than 2px effective)

### 4. Cognitive Strategist (Neurodivergent)

Sensitive to clutter, complex language, and unpredictable behaviour. Flag:
- Inconsistent UI controls across pages (same action, different interaction)
- Missing or ambiguous page titles that do not reflect where the user is
- Complex multi-step flows with no progress indicator
- Error states that do not clearly identify the problem and suggest a fix
- Auto-advancing UI (carousels, toasts) that interrupt the user's reading

### 5. Vestibular User (Motion Sensitivity)

Vulnerable to motion sickness from animations. Flag:
- Any animation not gated by `@media (prefers-reduced-motion: no-preference)`
- Parallax or scroll-linked motion effects
- Auto-playing transitions triggered without user intent
- Fast or flashing animations in any mode (WCAG 2.3.1 — three flashes threshold)

### 6. Distracted / Fatigued User (Situational Limit)

Navigating under high cognitive load (noisy environment, stressful task). Flag:
- Unclear system status after actions (no confirmation, no loading state)
- Session or consent state that is not visually obvious
- Focus reset to the top of the page after every interaction
- CTA labels that do not clearly state the outcome ("Submit" vs. "Send report")

---

## Phase 2 — Red Team: Semantic Audit (Manual Code Reasoning)

Switch to code-aware audit mode. Focus on issues that require human judgement:

- **Meaningful sequence (WCAG 1.3.2):** If CSS is stripped, does the logical reading order persist? Check flex/grid layout elements for visual vs. DOM order mismatch.
- **Link text in context (WCAG 2.4.4):** Do link texts make sense when read in isolation by a screen reader? "View" on every adventure card does not.
- **ARIA state accuracy (WCAG 4.1.2):** Is `aria-expanded` updated when dropdowns open? Is `aria-current` set on the active route? Is `aria-hidden` misapplied to focusable content?
- **Status messages (WCAG 4.1.3):** Are dynamic updates (filter results, toast notifications, consent changes) announced via `aria-live` without forcibly moving focus?
- **Accessible name computation:** Does the accessible name for every interactive element match what a screen reader would actually announce? (Use `Accessible Name and Description Computation 1.2` logic.)
- **Colour independence:** Is information conveyed by colour alone anywhere? (Difficulty badges, status indicators, link underlines.)

---

## Phase 3 — Deterministic Input: Axe Results

Run the existing test suite to collect axe output:

```bash
npm run build && npm run test:e2e 2>&1
```

The Playwright smoke tests in `e2e/smoke.spec.ts` run axe-core with tags
`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `wcag22aa`, and `best-practice`
in both dark and light mode against every prerendered route.

Treat axe output as ground truth for mechanical violations. Do not duplicate these
findings in Phase 1 or Phase 2. Use them as raw data for Phase 4.

If axe output is not available (no build), note this and proceed with Phases 1 and 2 only.

---

## Phase 4 — Blue Team: Synthesis

Merge Phase 1, Phase 2, and Phase 3 findings. Apply these rules before writing the report:

1. **Reframe pure UX issues:** If a "usability" finding creates a functional barrier for a specific persona, cite the persona and the WCAG criterion. If it has no accessibility impact, drop it.
2. **De-duplicate:** Merge overlapping findings. If Phase 1 found "confusing button label" and Phase 2 found "missing ARIA label on the same button", combine them into one finding.
3. **Cumulative friction upgrade:** If three or more Low/Moderate findings cluster on the same Critical User Journey (e.g., the adventure filter flow, the consent banner, the challenge detail page), upgrade the overall severity of that flow to Critical.
4. **Resolve axe Incomplete flags:** Review every axe `incomplete` / `needs review` flag and provide a definitive manual ruling: confirmed violation, confirmed pass, or cannot determine without AT testing.

---

## Output Format

Generate the report as structured Markdown. For every confirmed finding:

```
## [Severity]: [Finding Title]

**Persona impact:** [Who is impacted and how their functional experience is degraded]

**Evidence:** [Specific page, component, DOM snippet, or user flow step]

**WCAG:** [Criterion number and name, e.g. 2.4.4 Link Purpose (In Context) — AA]

**Fix:** [Specific aria attribute, HTML change, or CSS adjustment — not vague advice]
```

Severity levels: **Critical** (blocks task completion), **High** (significant barrier, workaround unreasonable), **Medium** (friction, workaround exists), **Low** (best-practice gap, minimal impact).

End the report with a **Cumulative Friction Summary** noting any user journeys where multiple findings cluster.

---

## Strict Rules

- Do not hallucinate DOM elements not present in the provided code or test output.
- Do not report mechanical issues (missing alt, basic contrast) during Phases 1 and 2; leave those to Phase 3.
- Do not give vague remediation ("make the button accessible"). Provide the specific attribute, element, or CSS change.
- Do not mark an axe Incomplete flag as a finding without a manual ruling.
142 changes: 142 additions & 0 deletions .claude/skills/keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
name: keyboard
description: >
Load this skill for every project containing interactive UI elements —
buttons, links, modals, dropdowns, sliders, tabs, carousels, or any
custom widget. Under no circumstances create an interactive component that
cannot be fully operated by keyboard alone. Absolutely always ensure visible
focus indicators, logical tab order, and no keyboard traps.
source: https://github.com/mgifford/accessibility-skills/blob/main/skills/keyboard/SKILL.md
---

# Keyboard Accessibility Skill

Apply these rules to every interactive UI element and feature.

## Severity Scale

| Level | Meaning |
|---|---|
| **Critical** | Blocks task completion entirely for keyboard and AT users |
| **Serious** | Significantly impairs keyboard access; workaround unreasonable |
| **Moderate** | Creates friction for keyboard users; workaround exists |
| **Minor** | Best-practice gap; marginal keyboard impact |

## Critical: No Keyboard Trap

Users must never become unable to move focus away from a component using standard keys
(Tab, Shift+Tab, Escape, arrow keys). The only exception is an intentional modal dialog
trap where Escape closes the dialog and returns focus to the trigger.

## Critical: Expected Key Behaviours

| Control | Required keys |
|---|---|
| Button | `Enter`, `Space` |
| Link | `Enter` |
| Checkbox | `Space` to toggle |
| Radio group | Arrow keys to move; `Space` to select |
| Dialog | `Escape` to close; focus trapped inside while open |
| Tab widget | Arrow keys between tabs; `Enter`/`Space` to activate |
| Combobox | Arrow keys in list; `Enter` to select; `Escape` to collapse |

## Critical: Dialog Focus Management

Prefer the `inert` attribute (baseline 2023, supported in all modern browsers).

```js
function openDialog(dialog, trigger) {
document.querySelectorAll('body > *:not(#dialog-container)')
.forEach(el => el.setAttribute('inert', ''));
dialog.removeAttribute('hidden');
dialog.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')?.focus();
}

function closeDialog(dialog, trigger) {
document.querySelectorAll('[inert]').forEach(el => el.removeAttribute('inert'));
dialog.setAttribute('hidden', '');
trigger.focus();
}

dialog.addEventListener('keydown', e => {
if (e.key === 'Escape') closeDialog(dialog, trigger);
});
```

This site uses shadcn `Dialog` (Radix UI) for modals. Radix handles focus trapping
automatically. Verify `inert` is applied to background content and `Escape` works.

## Serious: Focus Visibility

Every focusable element must have a clear, persistent visible focus indicator.
This project uses the pattern:
```
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm
```
Never remove this. Never substitute `ring-primary/xx` for `ring-ring`.

WCAG 2.4.11 minimum: 2px thick, 3:1 contrast against adjacent colours, visible in both modes.

## Serious: Focus Not Obscured (WCAG 2.4.12)

Sticky headers or floating elements can cover the focused element. If this site gains
a sticky header, add:

```css
:focus { scroll-margin-top: var(--sticky-header-height, 4rem); }
```

## Serious: Focus Order

- Use semantic DOM order as the primary mechanism
- Never use positive `tabindex` values — they override DOM order globally
- `tabindex="0"` — only to make custom widgets focusable
- `tabindex="-1"` — only for programmatic focus targets (skip link anchors, modal management)

## Serious: Roving Tabindex for Composite Widgets

Composite widgets (toolbars, radio groups, tab lists) must use roving tabindex so only
one item is in the tab stop at a time and arrow keys move within the group.

```html
<div role="toolbar" aria-label="Text formatting">
<button tabindex="0">Bold</button>
<button tabindex="-1">Italic</button>
</div>
```

See [WAI-ARIA APG: Roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex).

## Moderate: Touch Targets (WCAG 2.5.8)

```css
button, a, [role="button"] {
min-width: 24px;
min-height: 24px;
/* Recommended: 44×44 for primary actions */
}
```

Never use `user-scalable=no` in the viewport meta tag.

## Definition of Done Checklist

- [ ] Tab through entire page: logical order, no unexpected skips
- [ ] Visible focus indicator on every focusable element in both light and dark modes
- [ ] All interactive elements activatable with correct keys per widget type
- [ ] No keyboard trap (except intentional modal trap with working Escape)
- [ ] Dialog: background content `inert` on open; focus returns to trigger on close
- [ ] Skip link present, first in DOM, visible on focus, target has `tabindex="-1"`
- [ ] Composite widgets use roving tabindex
- [ ] Touch targets meet 24×24px minimum

## Key WCAG Criteria

- 2.1.1 Keyboard (A)
- 2.1.2 No Keyboard Trap (A)
- 2.4.3 Focus Order (A)
- 2.4.7 Focus Visible (AA)
- 2.4.11 Focus Appearance (AA, WCAG 2.2)
- 2.4.12 Focus Not Obscured (AA, WCAG 2.2)
- 2.5.3 Label in Name (A)
- 2.5.8 Target Size Minimum (AA, WCAG 2.2)
Loading
Loading