Skip to content

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.

8 token pairsAll ≥ 4.5:1 for textBorders are decorative only

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.

ForegroundBackgroundRatioUse
ForegroundBackground13.74:1Body text on the page
ForegroundSurface elevated12.96:1Text inside cards
Muted foregroundBackground5.84:1Helper, caption, label
AccentBackground4.62:1Editorial italic emphasis
DestructiveBackground4.51:1Error text
Muted foregroundSurface inset5.18:1Text on code blocks
BorderBackground1.43:1Decorative lines only (never text) — decorative only
Line subtleBackground1.10:1Decorative 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

  1. Body text uses --foreground. Never --muted-foreground for body.
  2. Captions, labels, and helper text use --muted-foreground at the smallest size (≥ 0.6875 rem).
  3. Errors use --destructive at 4.51:1, which is exactly at the AA threshold for body text.
  4. Accent text is reserved for editorial emphasis, never for body.
  5. 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.