Contrast checker

mandatory

In-browser tool at `kds.koder.dev/tools/contrast/` that validates foreground-background color pairs against WCAG 2.2 AA / AAA. Material parity (`/foundations/accessible-design/accessibility-basics` color tools). Used inline by Theme Builder; standalone for ad-hoc checks.

Spec — Contrast checker

Facet *ool*of Koder Design. Material parity: https://m3.material.io/foundations/accessible-design.

URL: https://kds.koder.dev/{locale}/tools/contrast/

What it does

Given two colors (foreground + background), output:

  • Contrast ratio (e.g., 4.7 : 1)
  • Pass / fail vs WCAG 2.2 AA (4.5 : 1 normal text, 3.0 : 1 large text)
  • Pass / fail vs WCAG 2.2 AAA (7.0 : 1 normal text, 4.5 : 1 large

    text)

  • Pass / fail vs non-text contrast (3.0 : 1 for UI components +

    graphical objects)

  • Suggested adjustments if failing — preserves hue, tweaks tone to

    pass

R1 — Input modes

Mode Source Use
*anual* Two hex / HCT pickers Ad-hoc check
*oken pair* Two token dropdowns Validate a color-roles.kmd pair
*ulk audit* Paste tokens JSON Run check on every role pair (Light + Dark)

R2 — Algorithm

Use *CAG 2.2 relative luminance*formula (sRGB):

contrast = (L1 + 0.05) / (L2 + 0.05)
where L1 = relative luminance of lighter color
      L2 = relative luminance of darker color

Relative luminance per WCAG: linearize each sRGB channel (c_lin = c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ^ 2.4), then L = 0.2126 * R + 0.7152 * G + 0.0722 * B.

Reference impl lives in tools/design-gen/internal/color/contrast.go.

APCA (next-gen contrast metric) is NOT used for pass / fail in v1 — WCAG 2.2 remains the legal standard. APCA shown as informational second metric only (opt-in toggle).

R3 — Output panel

┌─────────────────────────────────────────┐
│  Foreground  Background    Sample text  │
│  ████         ░░░░          AaBbCc 123   │
│  #1976D2      #FFFFFF                    │
│                                          │
│  Contrast ratio:           4.59 : 1     │
│                                          │
│  WCAG 2.2                                │
│  ✓ AA  normal text   (≥ 4.5)            │
│  ✓ AA  large text    (≥ 3.0)            │
│  ✗ AAA normal text   (≥ 7.0)            │
│  ✓ AAA large text    (≥ 4.5)            │
│  ✓ Non-text          (≥ 3.0)            │
│                                          │
│  Suggestion: shift FG to #1565C0 →       │
│  contrast 5.31 : 1 (passes AA AAA-large) │
└─────────────────────────────────────────┘
  • *ample text* rendered in both BG / FG with proper font sizes

    (16 px for "normal", 18 pt = 24 px or 14 pt bold = ~20 px for "large")

  • *ick / cross marks* tone uses success / error color roles

    (passing the contrast spec themselves)

R4 — Suggestion engine

If FG/BG fails AA, suggest a shifted FG (preserving hue):

  • Step tone in HCT space toward black or white in 5-unit increments
  • Pick the smallest shift that passes AA
  • Show before / after sidebyside; user picks or refines

When no shift passes (e.g., yellow on white), display:

"No tone shift preserves hue while passing AA. Consider a different role or surface."

R5 — Bulk audit (Theme Builder integration)

Input: full theme JSON (the tokens.json Style Dictionary file from tools/design-kit-export.kmd § R2).

Output table:

Role                              Ratio   AA   AAA
──────────────────────────────────────────────────
primary       /  on-primary       7.2 : 1  ✓    ✓
secondary     /  on-secondary     6.8 : 1  ✓    ✓
surface       /  on-surface       12.1 : 1 ✓    ✓
...
error-container / on-error-container  3.9:1  ✗   ✗   ← flagged

Failing rows highlighted in red; click to open Manual mode with that pair preloaded for tuning.

R6 — Token role validation

For a color-roles.kmd role pair (e.g., primary + on-primary), the checker enforces WCAG 2.2 AA as a *ard gate*when:

  • Pair is used for text (on-* roles)
  • Pair is used for icon (on-*-variant + *-container pairs)

For decorativeonly roles (e.g., `surfacetint`), no gate — checker shows ratio informationally.

R7 — Color-blindness simulation

Toggle: re-runs the check against simulated palette (Protanopia / Deuteranopia / Tritanopia / Achromatopsia).

If a pair passes WCAG 2.2 AA in normal vision but fails for one of the simulations, surface a soft warning ("Accessible to normal vision but low contrast for Deuteranopia (3.2 : 1)").

WCAG 2.2 does NOT require passing for simulated vision; this is a qualityofdesign warning, not a fail.

R8 — Accessibility of the checker itself

  • Tickers (✓ / ✗) accompanied by accessible text labels — color +

    symbol + text triple

  • Result ratio announced in live region on change
  • Color pickers from themes/color-customization.kmd-compatible HCT

    pickers (sliders are role="slider" etc.)

R9 — Performance

  • Computation < 5 ms per pair (pure math; runs at every input change)
  • Bulk audit: < 200 ms for 18 roles × 2 modes (Light + Dark) = 36

    pairs

R10 — Forbidden patterns

  • ❌ Using WCAG 2.0 (outdated); use 2.2
  • ❌ Returning APCA pass / fail as primary metric (WCAG remains the

    contract)

  • ❌ Suggesting colors outside the current preset's palette in

    Theme Builder context (use bulk audit then theme-tune flow)

  • ❌ Reporting non-text contrast as text (3.0 : 1 is too low for

    text)

  • ❌ Hardcoding sample text "Lorem ipsum" without i18n localization
  • themes/color-roles.kmd — role pair definitions
  • themes/color-customization.kmd — color-blindness packs
  • tools/theme-builder.kmd — embedded contrast checker
  • interaction/states.kmd — focused / hover overlays must also pass

    contrast against the underlying surface

  • i18n/contract.kmd — sample text locale

Source: ../home/koder/dev/koder/meta/docs/stack/specs/tools/contrast-checker.kmd