Skip to content
Open
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
115 changes: 115 additions & 0 deletions .agents/skills/accessibility/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
name: accessibility
description: >-
Guides frontend accessibility using Curve personas and WCAG-oriented rules:
semantic HTML, accessible names, keyboard access, focus, and persona-based
review. Use when writing or reviewing React, Angular, HTML, or CSS UI, or when
the user mentions a11y, accessibility, WCAG, toegankelijkheid, personas,
keyboard navigation, screenreader, or accessible names.
---

# Web accessibility (Curve)

Apply this skill whenever you **write, edit, or review** UI. Do not wait for the user to mention a11y.

Read [personas.md](personas.md) for the seven user perspectives. Read [reference.md](reference.md) for
detailed HTML, keyboard, and naming rules.

## When to use which mode

| Situation | Mode |
| --------- | ---- |
| Writing or editing UI | Baseline workflow (below) + relevant persona checks |
| "Review for accessibility" / PR review | **Full persona review** |
| "Review as [persona]" | **Single persona review** |
| Quick sanity check | Baseline workflow only |

## Baseline workflow (always)

When changing UI, run in order:

1. **Semantics** — correct HTML element (see [reference.md](reference.md)).
2. **Names** — every control has a visible label or accessible name.
3. **Structure** — one `h1`, no skipped heading levels, landmarks present.
4. **Keyboard** — Tab reaches every control; focus is visible.
5. **Skip links** — repeated chrome can be skipped on full pages.

Prefer native HTML before ARIA. If Curve components exist, use them instead of rebuilding primitives.

## Persona review workflow

Use this when reviewing a component, page, or story.

1. Read the code (and story/demo if present). Identify interactive elements, status feedback, media, and layout constraints.
2. Walk **each persona** in [personas.md](personas.md). For each one, ask that persona's review questions against the actual markup and behaviour.
3. Map findings to severity:
- **Must fix** — blocks a persona from using the UI (no name, keyboard trap, colour-only status, hover-only content)
- **Should fix** — friction or WCAG risk (weak contrast, small targets, vague errors)
- **Note** — improvement, not a blocker
4. End with **cross-cutting fixes** — changes that help multiple personas.

Do not answer "is this accessible?" with a single yes/no. Report concrete, verifiable findings.

### Full review output template

```markdown
# Accessibility review: [Component or page name]

## Blind, screenreadergebruiker
- [Must fix] …
- [Should fix] …

## Slechtziend, vergroting
- …

## Kleurenblind
- …

## Doof of slechthorend
- …

## Beperkte motoriek
- …

## Cognitief of neurodivergent
- …

## Tijdelijk of situationeel
- …

## Cross-cutting fixes
1. …
2. …
```

Omit empty persona sections. If a persona has no issues, write "Geen bevindingen" for that section.

### Single persona output

When the user names one persona, use the same severity labels but only that section plus cross-cutting fixes.

## Good prompts (for the user)

These produce verifiable answers:

- "Welk element krijgt focus als dit opengaat, en wat leest een screenreader voor?"
- "Kun je de foutstatus begrijpen zonder kleur te zien?"
- "Is elke actie bereikbaar met alleen het toetsenbord?"
- "Welk native element maakt dit ARIA-attribuut overbodig?"

Avoid accepting a bare "yes, accessible" without mechanism.

## Limits

This skill does not replace manual testing with VoiceOver/NVDA or `@storybook/addon-a11y`. Treat output
like a linter: fast and useful, but not a substitute for real assistive-tech checks.

## Installation (for humans)

| Tool | Path |
| ---- | ---- |
| Cursor | `.cursor/skills/accessibility/` (this folder) |
| Claude Code | `.claude/skills/accessibility/` |
| Other agents | `.agents/skills/accessibility/` |

Download the full folder from Curve Storybook: **Curve → Voor developers → Je werk testen → Wanneer je gebruik maakt van AI**.
122 changes: 122 additions & 0 deletions .agents/skills/accessibility/personas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Accessibility personas (Curve)

"Toegankelijk" gaat niet over één doelgroep. De persona's hieronder dekken de behoeften die de
WCAG-criteria daadwerkelijk sturen, en ze lopen elk op andere fouten vast. Gebruik ze om te
voorspellen welk criterium een ontwerp breekt door je voor te stellen wie er niet langs komt.

| Persona | Hoe diegene jouw website gebruikt | Waar het misgaat |
| ---------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------- |
| Blind, screenreadergebruiker | Alleen toetsenbord, luistert; navigeert via koppen en links | Naamloze knoppen, `div`-knoppen, betekenis die alleen zichtbaar is |
| Slechtziend, vergroting | 200–400% zoom, hoog contrast, grote letters | Vaste breedtes, te licht contrast, tekst in afbeeldingen |
| Kleurenblind | Ziet de opmaak, niet het kleurverschil | Status alleen in kleur, rood/groen-combinaties, ongelabelde grafieken |
| Doof of slechthorend | Leest in plaats van luistert | Video zonder ondertiteling, geluidssignalen, geen transcript |
| Beperkte motoriek | Toetsenbord, switch, spraak of oogbesturing | Kleine klikdoelen, alleen slepen, hover-menu's, korte time-outs |
| Cognitief of neurodivergent | Heeft rust, voorspelbaarheid en eenvoud nodig | Jargon, automatische beweging, tijdsdruk, vage foutmeldingen |
| Tijdelijk of situationeel | Van alles, kort en onhandig | Alles hierboven, op het slechtst denkbare moment |

## Blind, screenreadergebruiker

Gebruikt NVDA, JAWS, VoiceOver of TalkBack en ziet de opmaak nooit. De pagina wordt als structuur
opgenomen: een lijst met koppen, een lijst met links, landmarks, en dan pas inhoud. Visuele groepering
betekent niets als die niet in de opmaak zit.

**Wat dit van je vraagt:** echte koppen in de juiste volgorde, één `h1`, landmarks (`main`, `nav`,
`header`) in plaats van anonieme `div`s, een toegankelijke naam op elk bedienbaar element, en
alternatieve tekst die vertelt wat een afbeelding overbrengt in plaats van wat erop staat. Een
icoonknop die wordt voorgelezen als "knop" is een doodlopende weg.

**Review-vragen:**

- Wat leest een screenreader voor elk interactief element?
- Is de kopstructuur een logische inhoudsopgave?
- Zit essentiële informatie alleen in visuele opmaak (kleur, positie, icoon zonder label)?

## Slechtziend, vergroting

Zoomt tot 200% of verder, of zet het besturingssysteem op grote letters. Bij 400% zoom op een viewport
van 1280px is jouw desktoplayout nog ongeveer 320px breed — het WCAG-criterium Reflow zegt dat de
inhoud daar moet blijven werken, in één scrollrichting, zonder dat er iets wegvalt.

**Wat dit van je vraagt:** layouts die meebewegen in plaats van horizontaal scrollen, tekst die 200%
vergroting overleeft zonder over elkaar te vallen, contrast van minimaal 4,5:1 voor lopende tekst en
3:1 voor interface-elementen en iconen, en geen essentiële tekst die in een afbeelding zit.

**Review-vragen:**

- Blijft de layout bruikbaar bij 200–400% zoom?
- Is contrast voldoende voor tekst én UI-elementen?
- Val essential content weg of wordt afgesneden?

## Kleurenblind

Ongeveer 8% van de mannen en 0,5% van de vrouwen heeft een vorm van kleurenblindheid, meestal
rood/groen. Zij zien jouw opmaak prima; wat wegvalt is het onderscheid dat je in kleur hebt gestopt.

**Wat dit van je vraagt:** laat kleur nooit de enige manier zijn waarop je informatie communiceert. Combineer het met een icoon, een label,
een patroon of een positie. Een invoerveld dat rood kleurt is onzichtbaar; een veld dat rood kleurt,
een waarschuwingsicoon krijgt en de zin "Vul een geldig e-mailadres in" toont, werkt voor iedereen.

**Review-vragen:**

- Kun je status (fout, succes, geselecteerd) begrijpen zonder kleur te zien?
- Zijn grafieken en diagrammen ook zonder kleur te onderscheiden?

## Doof of slechthorend

Leest in plaats van luistert. Beschrijft ook iedereen in een kantoortuin zonder koptelefoon.

**Wat dit van je vraagt:** ondertiteling bij alles waarin gesproken wordt, transcripten bij audio, en geen
statuswijziging die alleen met geluid wordt aangekondigd.

**Review-vragen:**

- Is alle audio/video-inhoud ook leesbaar?
- Worden statuswijzigingen ook visueel of tekstueel aangekondigd?

## Beperkte motoriek

Een brede groep: tremor, RSI, bediening met één hand, een switch, spraakbesturing, oogbesturing. Wat ze
delen is dat nauwkeurig aanwijzen duur of onmogelijk is.

**Wat dit van je vraagt:** alles moet bereikbaar en bedienbaar zijn met het toetsenbord, in een volgorde
die overeenkomt met de opmaak, met focus die altijd zichtbaar is. Klikdoelen zijn minimaal 24×24
CSS-pixels (44×44 is comfortabel). Alles wat je met slepen kunt doen, moet ook met één klik of met het
toetsenbord kunnen. Inhoud die alleen bij hover verschijnt is onbereikbaar — koppel die ook aan focus,
en zorg dat je hem kunt wegklikken zonder de muis te verplaatsen.

**Review-vragen:**

- Is elke actie bereikbaar met Tab/Enter/Spatie?
- Zijn klikdoelen groot genoeg?
- Zijn er hover-only of drag-only interacties?

## Cognitief of neurodivergent

Dyslexie, ADHD, autisme, verschillen in geheugen of verwerkingssnelheid, angst — en iedereen onder
druk. Dit is de grootste groep en de groep die het minst geholpen wordt door automatische controles,
omdat niets ervan in de opmaak te zien is.

**Wat dit van je vraagt:** gewone taal in plaats of slimme taal, navigatie en knoppen die steeds op
dezelfde plek staan, geen onverwachte beweging of automatisch afspelen, respect voor
`prefers-reduced-motion`, ruime of verlengbare tijdslimieten, foutmeldingen die het probleem én de
oplossing noemen, en niets dat afhangt van wat iemand zich uit een vorige stap herinnert.

**Review-vragen:**

- Zijn foutmeldingen concreet en oplossingsgericht?
- Is de flow voorspelbaar, zonder verrassende animaties of time-outs?
- Is de taal eenvoudig genoeg?

## Tijdelijk of situationeel

Een gebroken arm, een ooginfectie, een kind op de arm, een gebarsten scherm, fel zonlicht, een tunnel.
Niemand hiervan noemt zichzelf beperkt, en ze lopen allemaal tegen dezelfde drempels aan.

Dit is het curb-cut-effect: ondertiteling wordt gebruikt in stille kantoren, sneltoetsen door ervaren
gebruikers, hoog contrast door mensen buiten. Werk dat je voor de persona's hierboven doet, is zelden
alleen voor hen.

**Review-vragen:**

- Welke persona's raken het hardst getroffen door dit ontwerp?
- Lost een fix voor één persona vaak meerdere problemen tegelijk op?
131 changes: 131 additions & 0 deletions .agents/skills/accessibility/reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Web accessibility reference

Detailed rules for semantic HTML, keyboard access, and accessible names. The agent applies these
when writing or reviewing UI (React, Angular, HTML, CSS).

Prefer native HTML before ARIA. Do not "fix" accessibility with `role="button"` + `tabindex="0"` +
a click handler on a `div` when a native `<button>` or `<a href>` works.

## Baseline workflow

When changing UI, run this in order:

1. **Semantics** — correct HTML element for the job (see Link vs button).
2. **Names** — every control has a visible label or an accessible name.
3. **Structure** — heading outline is a single logical tree (one `h1`, no skipped levels).
4. **Keyboard** — Tab reaches every interactive control; focus is always visible.
5. **Skip links** — repeated chrome can be skipped; targets are focusable.

When reviewing, report findings as:

- **Must fix** — keyboard trap, missing name, wrong element, outline removed, skipped heading level
- **Should fix** — missing hover/focus styles, `cursor: pointer` on a non-control, skip link missing
- **Note** — improvement, not a blocker

## Link vs button

| User intent | Element |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Go to another page, view, or URL (including in-app routes) | `<a href="...">` |
| Do something on this page: open a panel, open a dialog, submit, toggle, delete, capture | `<button type="button">` or `<button type="submit">` inside a form |

Rules:

- In-app navigation is still a link. Calling the router from a `<div onClick>` / `(click)` / `@click` or from a `<button>` is wrong when the destination is a route.
- The router helper must still render a real `<a href>`:
- **React:** `<Link>` / `<NavLink>` (React Router, Next.js). Do not replace the anchor with a `<span>` or `<div>`.
- **Angular:** `routerLink` on an `<a>`, not on a `<div>` or `<button>`.
- Mark the current page with `aria-current="page"` on the **link**, not on a wrapper.
- A button that looks like a link is still a `<button>`.
- A link that looks like a button is still an `<a href>`.

## Semantic HTML

Use native elements before ARIA:

- Headings (`h1`–`h6`) for titles, not bold `div`s
- `<button>` / `<a href>` for interaction, not clickable `div`/`span`
- `<label>` + form control, not placeholder-as-label
- `<nav>`, `<main>`, `<header>`, `<footer>`, `<aside>` for landmarks
- `<ul>`/`<ol>` for lists of links or findings
- Native `<dialog>`, or the design-system Dialog/Sheet primitive (keep its Title)
- Decorative images: `alt=""` and `aria-hidden="true"`

If you feel you must set `cursor: pointer` on a custom element, stop. Native `<a>` and `<button>` are already interactive. `cursor: pointer` on a `div`/`span`/`li` is a smell that the wrong element was used.

Allowed exceptions: `label` wrapping a control, `summary` in `<details>`, and components whose root is already a native control (design-system Button, etc.).

When the project has a design system (for example Curve), use its components instead of restyling generic elements. They already provide roles, keyboard behaviour, and focus rings.

## Headings

- Exactly one `h1` per view.
- Do not skip levels (`h1` → `h3`).
- Heading level follows **document structure**, not font size. Style with CSS.
- Use a visually hidden heading (`.sr-only` / `sr-only`) when a visual heading is missing but the section needs one (for example a sidebar labelled only by an icon).

Verify in Chrome DevTools: **Elements → Accessibility pane → Headings**. The map must read as a table of contents.

## Labels and accessible names

Every `input`, `select`, `textarea`, `button`, and `a` needs an accessible name.

Preferred order:

1. Visible `<label for="id">` matching the control's `id` (or wrap the control in `<label>`)
2. Visible text inside `<button>` / `<a>`
3. `aria-labelledby` pointing at existing visible text
4. `aria-label` only when a visible label would be redundant (icon-only controls)

Check in DevTools: **Elements → Accessibility pane → Name**. If Name is missing, the control is unnamed.

Do not rely on `placeholder` as the only name. Use a visually hidden label when the design has no visible label.

Translate user-facing strings, including `aria-label` and skip-link text.

## Keyboard

After building a view, walk it with **Tab** (and **Shift+Tab**):

- Every link, button, and input is reachable
- Order matches visual order
- No surprise tab stops on non-interactive text
- Focus is never lost when opening/closing a panel or dialog
- `tabindex` > 0 is forbidden
- `tabindex="-1"` is only for programmatic focus targets (skip-link targets, dialogs)

## Hover and focus

Every interactive element needs a `:hover` style **and** a `:focus` / `:focus-visible` style.

- **Never** remove the outline (`outline: none` / `outline: 0`) unless you replace it with an equally visible focus indicator.
- Prefer `:focus-visible` when the ring should appear for keyboard focus, not mouse clicks.
- `:focus` matches **any** focus (mouse, keyboard, script). `:focus-visible` matches when the browser judges a ring is needed (typically keyboard).
- If you use Curve, keep the component `focus-visible:ring-*` classes. Do not strip them to "clean up" the design.

## Skip links

Skip links are the first focusable links on a page. They let keyboard users jump past repeated chrome (sidebar, nav) to main content.

```html
<a class="skip-link" href="#main">Skip to main content</a>
<!-- site chrome -->
<main id="main" tabindex="-1">
<!-- page content -->
</main>
```

Rules:

- Put skip links near the start of the document (or the start of the sidebar/nav).
- `href` must point at a real `id`.
- The target must be focusable (`tabindex="-1"` on `<main>` / footer if it is not natively focusable).
- The link is visually hidden until focused — never `display: none` (that removes it from the tab order).

## Live status

Do not fail silently with a visual-only error. Announce status and errors with `aria-live` (or `role="status"` / `role="alert"`), or the design-system equivalent.

## Dialogs

Use a real dialog primitive (native `<dialog>`, or the design-system Dialog/Sheet). Always include a visible title; if the design hides it, keep the title in the DOM and hide it visually. Focus must move into the dialog when it opens and restore when it closes. Escape and a close control are required.
10 changes: 8 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ pnpm install # whole workspace
pnpm build # turbo: build both libraries
pnpm lint # turbo: type-check
pnpm format # prettier --write across the repo
pnpm --filter @surfnet/curve-react storybook # React Storybook (port 6006)
pnpm --filter @surfnet/curve-angular storybook # Angular Storybook (port 6007)
pnpm storybook # both Storybooks (React :6006, Angular :6007)
pnpm storybook:react # React Storybook (port 6006)
pnpm storybook:angular # Angular Storybook (port 6007)
```

Always run `pnpm lint` and `pnpm format` before considering a change done, and rebuild
Expand Down Expand Up @@ -194,6 +195,11 @@ Task-specific playbooks live in `.agents/skills/` (symlinked to `.claude/skills`
- **add-component** — (repo-authored) add a component to `@surfnet/curve-react`,
`@surfnet/curve-angular`, or both in parity. The `SKILL.md` index routes to the per-framework
playbooks `react.md` and `angular.md`.
- **accessibility** — (repo-authored) persona-based a11y review for agents. Canonical source:
`.agents/skills/accessibility/` (`SKILL.md`, `personas.md`, `reference.md`). Storybook serves
the same files under `/downloads/accessibility/` and ships a zip at
`packages/storybook-config/static/accessibility.zip`. Regenerate the zip after editing the skill:
`pnpm --filter @surfnet/curve-storybook-config bundle:accessibility-skill`.
- **shadcn** — (upstream, from `shadcn/ui`) deep reference for shadcn components, registries,
presets, and Base-vs-Radix.
- **spartan** — (upstream, from `spartan-ng/spartan`) deep reference for spartan/ui, the
Expand Down
Loading
Loading