Skip to content

Feature: color-blind contrast lint rule (opt-in, Brettel-Viénot-Mollon simulation) #48

@oknemixam

Description

@oknemixam

Summary

Proposing an opt-in lint rule color-blind-contrast that simulates protanopia, deuteranopia, and tritanopia via standard deficiency matrices and re-runs the contrast check on the simulated colors. Catches palettes where the only cue is a color shift invisible to ~5–10% of users with congenital color-vision deficiency (CVD).

Why this matters

Baseline contrast-ratio (WCAG AA 4.5:1) checks contrast as perceived by full-color vision. A component that passes AA for a tritanopic user can still fail at 2–3:1 for a protanopic user if the palette's primary accent lives in the red-green axis, because protanopia compresses that range.

Real example from our own pilot project:

  • Brand orange #E44001 on white #FFFFFF: contrast 4.17:1 (baseline flags this as AA-near-miss)
  • Same pair under protanopia simulation (#9D9C10 on #FFFFFF): contrast 2.92:1 — below even the 3:1 large-text floor
  • Same pair under deuteranopia simulation (#A7B314 on #FFFFFF): contrast 2.31:1 — effectively invisible

The author only sees the baseline 4.17:1 warning and may rationalize it as AA-Large-only, not realizing the component is unusable for ~5% of their male audience in any size.

Proposed rule

color-blind-contrast (warning, opt-in)

For each component with a backgroundColor/textColor pair, apply each deficiency matrix to both colors, recompute contrast on the simulated pair, warn if ratio < 3:1.

warning [color-blind-contrast]: component 'button-primary' contrast 2.92:1
  under protanopia simulation (bg #9d9c10 on fg #ffffff) is below 3:1 floor
  — relying on color-only cues fails this audience

Simulation matrices

Published Brettel-Viénot-Mollon approximations. Applied to normalized sRGB [0,1]:

const MATRICES = {
  protanopia:   [[0.567, 0.433, 0.0], [0.558, 0.442, 0.0], [0.0, 0.242, 0.758]],
  deuteranopia: [[0.625, 0.375, 0.0], [0.7,   0.3,   0.0], [0.0, 0.3,   0.7]],
  tritanopia:   [[0.95,  0.05,  0.0], [0.0,   0.433, 0.567], [0.0, 0.475, 0.525]],
};

These are the simplified matrices commonly cited in a11y tooling (e.g. Stark, Figma's CVD plugin). Sufficient for flagging high-risk palettes, not a substitute for live testing with CVD users.

Why opt-in

CVD simulation is a stricter bar than baseline WCAG AA. Projects that have explicitly prioritized it should declare intent:

x-oknemix-a11y-target: AAA
# OR a dedicated toggle
lint-rules:
  color-blind-contrast: warning

Opt-in avoids noise for projects that haven't made the decision yet, while giving a clear upgrade path.

Implementation notes

Working in our @google/design.md bridge today:

  • a11y.ts — 3 matrices + contrast re-check, ~40 lines for the CB rule specifically
  • test-a11y.ts 24/24 passing (CB rule is 4 of those tests)
  • Currently flagging real accessibility regressions on consumer products

Happy to PR as an opt-in rule in DEFAULT_RULES with a sensible default severity, or as an exportable rule for runLinter({ rules: [...custom, colorBlindContrast] }).

Prior art / reading

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions