Repository navigation
Description
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
#E44001on white#FFFFFF: contrast 4.17:1 (baseline flags this as AA-near-miss) - Same pair under protanopia simulation (
#9D9C10on#FFFFFF): contrast 2.92:1 — below even the 3:1 large-text floor - Same pair under deuteranopia simulation (
#A7B314on#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: warningOpt-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 specificallytest-a11y.ts24/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
- Brettel, Viénot & Mollon (1997): https://www.researchgate.net/publication/14397480
- Color Oracle: https://colororacle.org/
- WCAG 2.2 Guideline 1.4.1 Use of Color: https://www.w3.org/WAI/WCAG22/Understanding/use-of-color
- Stark CVD simulation: https://www.getstark.co/features/color-blind-simulator/
Activity
mvanhorn commentedon Jun 26, 2026
Opened a PR for this: #133
It adds the rule as you specified, opt-in behind a
--cvdflag onlint, 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.feat(linter): CVD contrast rule and unverified-contrast info
drmikecrypto commentedon Jun 27, 2026
Opened #142 with opt-in
color-blind-contrast(Brettel-Viénot-Mollon matrices, 3:1 floor) plus defaultunverified-contrastinfo 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
--cvdCLI flag instead.feat(linter): CVD contrast rule and unverified-contrast info