Apply WCAG 2 Level AA accessibility standards to design systems, UI components, and web pages — contrast math with a runnable validator, design-token patterns that make color accessible by construction, typography and focus-state rules, and a semantics checklist. Use when building or reviewing UI, c
---
name: web-accessibility
description: Apply WCAG 2 Level AA accessibility standards to design systems, UI components, and web pages — contrast math with a runnable validator, design-token patterns that make color accessible by construction, typography and focus-state rules, and a semantics checklist. Use when building or reviewing UI, choosing colors or type for a brand/design system, setting up design tokens, wiring accessibility checks into a build, or when the user mentions accessibility, a11y, WCAG, contrast, or screen readers.
license: MIT
metadata:
author: raunakkathuria
version: "1.0"
---
# Web Accessibility (WCAG 2 AA)
Make interfaces conform to [WCAG 2 Level AA](https://www.w3.org/WAI/WCAG2AA-Conformance) by building
accessibility into the design system, not auditing it in afterwards. A color, text style, or
component that enters the system already passing AA scales to every page that uses it; one that
doesn't becomes a bug on every page at once.
Conformance target: WCAG 2.2 Level AA ([quick reference](https://www.w3.org/WAI/WCAG22/quickref/?currentsidebar=%23col_customize&levels=aaa)).
## The order of operations
1. **Tokens first** — validate the palette as fg/bg pairs before any component uses it.
2. **Components second** — states, focus, target size, semantics per component.
3. **Pages last** — structure, headings, alt text, keyboard walk-through.
Fixing a failing token fixes every component; fixing a failing page fixes one page. Work top-down.
## 1. Color contrast is math, not judgment
Never eyeball contrast. The thresholds (WCAG 2.2):
| What renders | Minimum | Success criterion |
|---|---|---|
| Normal text (below 24px / below 18.66px bold) | **4.5:1** | 1.4.3 |
| Large text (≥24px, or ≥18.66px bold) | **3:1** | 1.4.3 |
| Meaningful non-text (icons, chart marks, input borders, focus indicators) | **3:1** | 1.4.11 |
| Disabled controls, decorative elements, logos | exempt | — |
Run the bundled validator (no dependencies, Node ≥14):
```bash
# ad-hoc pairs: fg,bg,minimum,label
node scripts/check-contrast.mjs --pair "#0f766e,#ffffff,4.5,link on white"
# a whole palette from a JSON config (see script header for the format)
node scripts/check-contrast.mjs contrast-pairs.json
```
**Wire it into the build.** A contrast check that runs once is an audit; one that runs in
`npm run build` or CI and exits non-zero is a guarantee. Add every token pair that renders as text
or meaningful graphics, in every theme.
## 2. Token patterns that make color accessible by construction
Three patterns cover most real-world palette failures:
- **Split fill roles from text roles.** Vibrant brand colors (a teal, an orange, a mid-blue) usually
pass as a *fill behind dark text* but fail as *text on a light background* — often by a factor of
two. Define both and never let components cross them:
```css
--color-primary: #14b8a6; /* fills, borders, focus rings — never text on light */
--color-primary-text: #0f766e; /* the same hue, dark enough to be text (≥4.5:1) */
```
Filled buttons then choose text *per theme*: dark text on a bright fill, or light text on a dark
fill — whichever pair passes, verified by the script, not by taste.
- **Theme-tune semantic colors.** Success/warning/error values that read well on near-black
(`#f59e0b` amber, `#ef4444` red) fail on white — amber is ~2.2:1 there. Map
`--color-success/warning/error` per theme instead of sharing one set; keep the bright ramp for
dark themes and a darker ramp (e.g. `#b45309`, `#dc2626`) for light.
- **Subtle text has a floor.** Placeholder, hint, and caption text is still text — 4.5:1 applies.
Mid-grays like `#9ca3af` on white (~2.5:1) are the single most common violation on the web. The
lightest AA gray on white is around `#767676`; pick your "subtle" token at or below that
lightness.
## 3. Typography that scales
- **Relative units** (`rem`/`em`) for font sizes, line heights, and containers that hold text —
browser zoom and text-resize must work to 200% without loss of content (SC 1.4.4).
- **Body text ≥ 1rem (16px)**; don't fight user font-size settings with `html { font-size: 62.5% }`
tricks that bake in pixel assumptions.
- **Line height ≥ 1.5** for body copy; paragraph spacing larger than line spacing (SC 1.4.12 expects
content to survive these being user-forced).
- **Don't convey meaning by color alone** (SC 1.4.1): pair color with an icon, weight, underline, or
text label — links inside prose need more than a hue change.
## 4. Interactive states
- **`:focus-visible` on every interactive element** — a 2px+ outline with 3:1 contrast against the
surrounding surface (SC 2.4.7, 2.4.13). Never `outline: none` without a replacement.
- **Hover/active recolors must also pass.** A link that darkens on hover passes twice; one that
brightens toward the background fails exactly when the user is looking at it. Add state colors to
the contrast config too.
- **Target size ≥ 24×24 CSS px** (SC 2.5.8) — buttons, nav links, icon buttons. 44px stays the
comfortable default for primary controls.
- **Respect `prefers-reduced-motion`** — gate non-essential animation behind the media query.
## 5. Semantics checklist (the part tokens can't fix)
Run through this per component/page; details and fix patterns in
[references/wcag-checklist.md](references/wcag-checklist.md):
- Images: `alt` text that says what the image *does* here; `alt=""` for decorative (1.1.1)
- One `<h1>`, headings in order, no skipping levels for styling (1.3.1)
- Real elements: `<button>`, `<a href>`, `<label for>` — not styled `<div>`s (4.1.2)
- Every input labelled; errors identified in text, not color alone (3.3.1, 3.3.2)
- Full keyboard walk-through: reach, operate, and *leave* everything; logical tab order; skip link
on page-level nav (2.1.1, 2.1.2, 2.4.1)
- Current page/state marked (`aria-current="page"`, `aria-expanded`, …)
- Page `lang` attribute and a descriptive `<title>` (3.1.1, 2.4.2)
## Honest boundaries
- Automated checks (this script, axe, Lighthouse) catch roughly a third to half of WCAG failures —
the mechanical ones. Keyboard walk-throughs and a screen-reader pass (VoiceOver/NVDA) are still
required for real conformance. Say "the token layer is AA-verified", not "the product is
accessible", until both have happened.
- A design system guarantees the **visual layer**: contrast, focus visibility, scalable type,
target sizes. Content semantics — alt text, heading order, labels, keyboard flows — live in each
product and must be checked there.
- Contrast ratios apply to the colors *as rendered* — opacity, gradients, and text over images need
checking at their worst point, not their average.
don't have the plugin yet? install it then click "run inline in claude" again.
added explicit inputs and external connection setup, broke monolithic original guidance into numbered procedure steps with input/output per step, extracted implicit decision branches (color fails contrast, semantic color in light theme, focus visibility trade-offs, token-early vs. page-late fixes), documented edge cases (tool conflicts, automation limits, opacity and gradients rendering variably), and added clear outcome signals that let users know accessibility actually worked.
---
name: web-accessibility
description: Apply WCAG 2 Level AA accessibility standards to design systems, UI components, and web pages , contrast math with a runnable validator, design-token patterns that make color accessible by construction, typography and focus-state rules, and a semantics checklist.
license: MIT
metadata:
author: raunakkathuria
version: "1.0"
---
# Web Accessibility (WCAG 2 AA)
## intent
build accessibility into the design system from the token layer up, not audit it in after launch. make interfaces conform to WCAG 2.2 Level AA by validating color contrast as math (not judgment), locking in accessible token patterns before any component uses them, and checking semantics and keyboard navigation at the page level. use this skill when building or reviewing UI, choosing colors or type for a brand or design system, setting up design tokens, wiring accessibility checks into a build pipeline, or when the user mentions accessibility, a11y, WCAG, contrast, screen readers, or keyboard navigation.
## inputs
- **Node.js** ≥14 (for the contrast validator script)
- **design token config** (JSON or CSS Custom Properties defining color pairs, typography scales, and spacing)
- **UI components or pages** to validate (HTML, component code, design mockups)
- **design system** or style guide (existing palette, typography rules, focus states)
- **CI/build pipeline** (optional, for automating contrast checks on every commit)
**external connections / setup:**
- no external APIs required. all checks run locally.
- contrast validator script (included): `scripts/check-contrast.mjs` (Node.js, no dependencies beyond Node stdlib).
- optional: axe DevTools, Lighthouse CI, or NVDA/VoiceOver for manual screen-reader testing (not required for this skill, but recommended for full conformance).
## procedure
### phase 1: validate color tokens (top-down)
**step 1:** gather all foreground/background color pairs that will render as text or meaningful graphics (icons, chart marks, input borders, focus indicators).
- *input:* design tokens (CSS, JSON, or Figma variables); list of theme variants (light, dark, high-contrast).
- *output:* a map of all fg/bg pairs per theme.
**step 2:** run the contrast validator script on each pair.
- *input:* fg color (hex or rgb), bg color (hex or rgb), minimum ratio (4.5 for normal text, 3 for large text or non-text graphics), human-readable label.
- *command:* `node scripts/check-contrast.mjs --pair "#0f766e,#ffffff,4.5,primary text on white"`
- *output:* pass/fail verdict; computed contrast ratio (e.g. "5.2:1, PASS").
**step 3:** batch-validate a whole palette from a JSON file.
- *input:* JSON file with structure: `{ "theme": { "pairs": [{"fg": "#...", "bg": "#...", "min": 4.5, "label": "..."}, ...] } }`
- *command:* `node scripts/check-contrast.mjs contrast-pairs.json`
- *output:* per-pair pass/fail; summary report; exit code non-zero if any fail.
**step 4:** wire the validator into CI/build.
- *input:* build script (`npm run build`, GitHub Actions, GitLab CI, etc.) and the JSON palette file.
- *action:* add `node scripts/check-contrast.mjs contrast-pairs.json || exit 1` to your pre-commit hook, build step, or CI job.
- *output:* build fails if any contrast pair fails; passes only if all pairs pass.
### phase 2: design token patterns that guarantee accessibility
**step 5:** split fill roles from text roles for brand colors.
- *input:* a vibrant brand color (e.g. teal `#14b8a6`).
- *action:* define two tokens: one for fills/borders/focus rings (`--color-primary`), one for text on light backgrounds (`--color-primary-text`). the text version must be darker (same hue family, darker value).
- *pattern:*
```css
--color-primary: #14b8a6; /* fills, borders, focus rings , never text on light */
--color-primary-text: #0f766e; /* dark enough to be text (≥4.5:1 on white) */
step 6: theme-tune semantic colors (success, warning, error).
#f59e0b amber, #ef4444 red) for dark themes; use darker ramps (e.g. #b45309, #dc2626) for light themes./* light theme */
--color-success: #16a34a;
--color-warning: #b45309;
--color-error: #dc2626;
/* dark theme */
--color-success: #86efac;
--color-warning: #f59e0b;
--color-error: #ef4444;
step 7: establish a floor for subtle text (placeholder, hint, caption).
#767676 or darker). use this as the floor for all text; do not use mid-grays like #9ca3af for body or hint text.--color-text-subtle or similar token locked at or below the AA floor.step 8: define font sizes and line heights in relative units.
rem or em, based on a 16px root (html { font-size: 1rem }). set line-height ≥1.5 for body copy; paragraph spacing larger than line spacing.html { font-size: 1rem; } /* 16px base, not 62.5% tricks */
body {
font-size: 1rem;
line-height: 1.6;
}
h1 {
font-size: 2.5rem;
line-height: 1.2;
}
step 9: pair color with additional cues (icon, weight, underline, text).
step 10: add :focus-visible with 3:1 contrast.
<button>, <a>, <input>, etc.).:focus-visible { outline: 2px solid <focus-color>; outline-offset: 2px; }. validate the focus color against the surrounding surface (e.g. blue outline on a white page = 3:1 minimum).button:focus-visible {
outline: 2px solid #0f766e;
outline-offset: 2px;
}
step 11: validate hover and active colors.
contrast-pairs.json and validated in CI.step 12: enforce target size ≥24×24 CSS px.
button, a, input[type="checkbox"] {
min-width: 24px;
min-height: 24px;
/* or use padding to achieve this */
}
step 13: gate animation behind prefers-reduced-motion.
@media (prefers-reduced-motion: no-preference) { animation: ... } or provide a toggle.@media (prefers-reduced-motion: no-preference) {
button { transition: background-color 200ms; }
}
step 14: run the semantics checklist per component and page.
<img> has alt text that describes what the image does in context; decorative images use alt="".<h1>; heading levels do not skip (h1 → h2/h3, not h1 → h4); heading hierarchy matches content structure.<button>, <a href>, <label for>, <input>), not styled <div>s.<label> with matching for attribute or wrapped around the input; error messages identified by text and visually, not color alone.aria-current="page"; expanded/collapsed state marked with aria-expanded; disabled buttons marked disabled or aria-disabled.<html lang="en"> (or appropriate language code); <title> descriptive and unique per page.step 15: keyboard walk-through.
if color fails 4.5:1 on white:
if a semantic color (success, warning, error) fails in light theme:
--color-error-light: #dc2626) and use it conditionally.if focus outline is hard to see against the background:
if a page element is not keyboard-reachable:
<button>, <a href>, <input>, etc.) or has tabindex="0", or<div> behave as a button (role="button", keypress handler for Enter/Space), but prefer semantic HTML.if the user mentions "we'll fix accessibility later":
if automated checkers pass but the product still feels broken:
if contrast validators conflict (e.g. different tools give different ratios for the same color):
tokens validated and locked:
contrast-pairs.json or equivalent) with all fg/bg pairs per theme, each with computed contrast ratio and pass/fail status.components with accessibility guarantees:
prefers-reduced-motion.pages passing semantics:
<h1> per page; heading order correct; real semantic elements throughout.<html lang>, page <title>, and aria-current="page" (or equivalent state markers) in place.sign-off artifacts (optional but recommended):
npm run build succeeds only if all contrast pairs pass the validator. no surprise failures in production.<button>, <a>, <label>, <h1>-<h6>, real <form> and <input> elements, not styled <div>s with click handlers.credits: original skill authored by raunakkathuria (clawhub). enriched per implexa quality standards with explicit procedure steps, decision points, edge cases, and outcome signals. ```