rules
React Accessibility
Enforce WCAG 2.1 AA for React UI: semantic HTML, keyboard support, ARIA, contrast, and accessible shadcn/Radix patterns.
ruledev-ted
ted-craft — zsh
$ npx ted-craft add react-accessibility -a cursor -g -y---
description: Accessibility — WCAG 2.1 AA for React UI
globs:
- "**/*.{tsx,jsx}"
alwaysApply: true
---
# Accessibility — WCAG 2.1 AA
**Assumes:** React UI; works best with shadcn/ui (Radix) primitives. Pair with `react-styling` and `react-shadcn-components` when those are installed.
New and updated UI should target **WCAG 2.1 Level AA**. Prefer patterns that work with keyboard, screen readers, and system settings.
## Stack-Specific Defaults
- Prefer **accessible primitives** (e.g. shadcn/ui / Radix) for `Dialog`, `AlertDialog`, `DropdownMenu`, `Select`, `Tabs`, etc. **Do not** remove or fight their built-in **focus trap**, **focus restoration**, `aria-*`, or keyboard behaviour unless replacing it with an equally accessible pattern.
- Treat **ESLint** accessibility rules (e.g. **jsx-a11y**, framework eslint configs) as required fixes for new code, not noise to disable.
- Use the framework’s **Link** component with a real `href` for in-app navigation (not `<button>` pretending to be navigation).
- Use optimized **Image** components with a correct **`alt`**: descriptive for meaningful images, **`alt=""`** for decorative. Do not omit alt on meaningful images.
## Semantic HTML
- Use the correct element for its purpose: **`nav`**, **`main`**, **`header`**, **`footer`**, **`article`**, **`section`** (with heading where it groups content), **`h1`–`h6`** in order (no skipped levels), **`ul` / `ol` / `li`**, **`table`** with **`th scope`**, **`button`** for actions, **`a`** for links.
- Do not use **`div` / `span`** as interactive controls unless you supply an appropriate **`role`**, keyboard support (**Enter/Space** as needed), and visible focus—default is to use **`<button>`** or Radix/shadcn triggers instead.
## ARIA
- **`aria-label`** or **`aria-labelledby`** on controls with no visible text (e.g. icon-only buttons).
- **`aria-describedby`** linking inputs to hints and **error text**; use **`aria-invalid`** when validation fails.
- Use **`role`** only when native HTML cannot express the control. Avoid redundant ARIA (e.g. **`role="button"`** on **`button`**).
- **`aria-live="polite"`** (or **`assertive`** for urgent errors) on regions for **dynamic updates** (e.g. toast stack, inline search results).
- **`aria-busy="true"`** on regions that are loading when skeleton/spinner content is shown and no better pattern exists.
## Keyboard
- Every interactive control must be **focusable and operable** with keyboard alone.
- **Tab order** follows visual order; use **`tabIndex={0}`** only when necessary; **never** positive **`tabIndex`**.
- **Modals / dialogs**: trap focus while open; return focus to the trigger on close (Radix/shadcn handle this when used as documented).
- **Menus / comboboxes**: support **arrow keys** / **Escape** where the pattern expects it (follow Radix/shadcn behaviour).
## Colour & Contrast
- Normal text: **≥ 4.5:1**; large text / important UI graphics: **≥ 3:1** against adjacent colours. Prefer **theme tokens** over ad hoc colours; avoid low-contrast **muted-on-muted** pairings for body text.
- Do not rely on **colour alone** for state (success/error)—add text, icons, or labels.
- **Focus rings** must be visible (`outline`, `ring`, or component defaults). Do not use **`outline-none`** without an accessible replacement.
## Images & Icons
- **`<img>`**: always **`alt`** (descriptive or **`""`** if decorative).
- **SVG / icon libraries** inside buttons or links: **`aria-hidden`** on decorative icons; the **control** carries **`aria-label`** or visible text. If the icon is the only content, **`aria-label`** on the **`button`** / **`Link`**.
## Forms
- Every control has an **associated label** (`Label` + `htmlFor` / `id`, or wrapping pattern used in the codebase).
- Errors are **programmatically associated** (`aria-describedby`, **`aria-invalid`**).
- **Required** fields: clear visually and with **`required`** / **`aria-required`**.
## Motion
- Respect **`prefers-reduced-motion`**: use Tailwind **`motion-safe:`** / **`motion-reduce:`** (or CSS media query) for non-essential animation.
## Checklist Before Completing UI Work
- [ ] Keyboard: all actions reachable and usable without a pointer
- [ ] Headings and landmarks make sense in order
- [ ] Images have correct **`alt`**
- [ ] Form fields have labels and linked error text
- [ ] Contrast sufficient for text and critical UI; focus visible
- [ ] Dynamic status content uses a live region where appropriate
- [ ] Dialogs/menus use accessible primitives (focus trap, Escape)
- [ ] No positive **`tabIndex`**Accessibility — WCAG 2.1 AA
Assumes: React UI; works best with shadcn/ui (Radix) primitives. Pair with react-styling and react-shadcn-components when those are installed.
New and updated UI should target WCAG 2.1 Level AA. Prefer patterns that work with keyboard, screen readers, and system settings.
Stack-Specific Defaults
- Prefer accessible primitives (e.g. shadcn/ui / Radix) for
Dialog,AlertDialog,DropdownMenu,Select,Tabs, etc. Do not remove or fight their built-in focus trap, focus restoration,aria-*, or keyboard behaviour unless replacing it with an equally accessible pattern. - Treat ESLint accessibility rules (e.g. jsx-a11y, framework eslint configs) as required fixes for new code, not noise to disable.
- Use the framework’s Link component with a real
hreffor in-app navigation (not<button>pretending to be navigation). - Use optimized Image components with a correct
alt: descriptive for meaningful images,alt=""for decorative. Do not omit alt on meaningful images.
Semantic HTML
- Use the correct element for its purpose:
nav,main,header,footer,article,section(with heading where it groups content),h1–h6in order (no skipped levels),ul/ol/li,tablewithth scope,buttonfor actions,afor links. - Do not use
div/spanas interactive controls unless you supply an appropriaterole, keyboard support (Enter/Space as needed), and visible focus—default is to use<button>or Radix/shadcn triggers instead.
ARIA
aria-labeloraria-labelledbyon controls with no visible text (e.g. icon-only buttons).aria-describedbylinking inputs to hints and error text; usearia-invalidwhen validation fails.- Use
roleonly when native HTML cannot express the control. Avoid redundant ARIA (e.g.role="button"onbutton). aria-live="polite"(orassertivefor urgent errors) on regions for dynamic updates (e.g. toast stack, inline search results).aria-busy="true"on regions that are loading when skeleton/spinner content is shown and no better pattern exists.
Keyboard
- Every interactive control must be focusable and operable with keyboard alone.
- Tab order follows visual order; use
tabIndex={0}only when necessary; never positivetabIndex. - Modals / dialogs: trap focus while open; return focus to the trigger on close (Radix/shadcn handle this when used as documented).
- Menus / comboboxes: support arrow keys / Escape where the pattern expects it (follow Radix/shadcn behaviour).
Colour & Contrast
- Normal text: ≥ 4.5:1; large text / important UI graphics: ≥ 3:1 against adjacent colours. Prefer theme tokens over ad hoc colours; avoid low-contrast muted-on-muted pairings for body text.
- Do not rely on colour alone for state (success/error)—add text, icons, or labels.
- Focus rings must be visible (
outline,ring, or component defaults). Do not useoutline-nonewithout an accessible replacement.
Images & Icons
<img>: alwaysalt(descriptive or""if decorative).- SVG / icon libraries inside buttons or links:
aria-hiddenon decorative icons; the control carriesaria-labelor visible text. If the icon is the only content,aria-labelon thebutton/Link.
Forms
- Every control has an associated label (
Label+htmlFor/id, or wrapping pattern used in the codebase). - Errors are programmatically associated (
aria-describedby,aria-invalid). - Required fields: clear visually and with
required/aria-required.
Motion
- Respect
prefers-reduced-motion: use Tailwindmotion-safe:/motion-reduce:(or CSS media query) for non-essential animation.
Checklist Before Completing UI Work
- Keyboard: all actions reachable and usable without a pointer
- Headings and landmarks make sense in order
- Images have correct
alt - Form fields have labels and linked error text
- Contrast sufficient for text and critical UI; focus visible
- Dynamic status content uses a live region where appropriate
- Dialogs/menus use accessible primitives (focus trap, Escape)
- No positive
tabIndex