AccessibilityContrast
Contrast
Every text-on-background pair in the system clears WCAG 2.1 AA. The math is short, the rules are short, and the audit is on every PR.
The eight pairs
All ratios measured against the warm-neutral --background (#F6F3EF) or against --surface-elevated (#FAF8F5) where the pair is used inside a card.
| Foreground | Background | Ratio | Use |
|---|---|---|---|
Foreground | Background | 13.74:1 | Body text on the page |
Foreground | Surface elevated | 12.96:1 | Text inside cards |
Muted foreground | Background | 5.84:1 | Helper, caption, label |
Accent | Background | 4.62:1 | Editorial italic emphasis |
Destructive | Background | 4.51:1 | Error text |
Muted foreground | Surface inset | 5.18:1 | Text on code blocks |
Border | Background | 1.43:1 | Decorative lines only (never text) — decorative only |
Line subtle | Background | 1.10:1 | Decorative dividers only (never text) — decorative only |
Decorative contrast
Borders and lines fail contrast on purpose. They are decorative elements that never carry information. A 1.43:1 line is a deliberate choice — louder lines look like corporate chrome; the--line-subtle / --line-strong pair is designed to recede.
Rules of thumb
- Body text uses
--foreground. Never--muted-foregroundfor body. - Captions, labels, and helper text use
--muted-foregroundat the smallest size (≥ 0.6875 rem). - Errors use
--destructiveat 4.51:1, which is exactly at the AA threshold for body text. - Accent text is reserved for editorial emphasis, never for body.
- No white-on-cream, no ink-on-ink. The system is designed for a single light palette.
Pre-PR checklist
- Run
npx @axe-core/cli http://localhost:3000/[page]on every page you touched. - If you used a color value not in
app/globals.css, justify it in the PR description. - If the component has an icon-only button, verify the icon is decorative and the button has an
aria-label. - If the component has disabled state, verify the disabled state is communicated by both the attribute and the reduced contrast.