Skip to content

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

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

Activity

  1. mvanhorn commented on Jun 26, 2026

    @mvanhorn
    Contributor

    Opened a PR for this: #133

    It adds the rule as you specified, opt-in behind a --cvd flag on lint, using the Brettel-Vienot-Mollon matrices and the 3:1 floor. The protanopia/deuteranopia figures from your worked example (2.92:1 on #9d9c10, 2.31:1 on #a7b314) are covered by a test.

  2. added a commit that references this issue on Jun 27, 2026
    41927f0
  3. drmikecrypto commented on Jun 27, 2026

    @drmikecrypto
    Contributor

    Opened #142 with opt-in color-blind-contrast (Brettel-Viénot-Mollon matrices, 3:1 floor) plus default unverified-contrast info when a palette has no component contrast pairs.

    The protanopia/deuteranopia figures from the worked example are covered by tests. Happy to adjust severity or default rule composition if maintainers prefer a --cvd CLI flag instead.

  4. added a commit that references this issue on Jun 28, 2026
    4a513d2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions