Checking Color Before You Ship
Simulation and contrast checks catch problems mockups hide.
QA signed off on pixels. Marketing approved the hero. No one opened the production build with deuteranopia simulation until a support ticket arrived: a user could not tell which dashboard row was failed versus pending because both states read as the same muddy brown. The palette had passed WCAG contrast checks on paper. The checks ran on the designer’s calibrated display against sRGB swatches. Nobody tested whether the meaning survived when hue discrimination collapsed.
Color accessibility failures are cheap to prevent and expensive to explain to leadership after launch. They split into two categories teams conflate: readability, meaning can you see the text, and discriminability, meaning can you tell what it means. Contrast ratios solve the first. Color-vision QA solves the second. This essay covers the biology briefly, the three deficiency types simulation tools model, how to run checks in DevTools and CI, a stakeholder review process that produces evidence instead of opinions, and the cases where simulation actively misleads you.
What simulation models and the minimum bar before it
Roughly eight percent of men and half a percent of women have some form of inherited color vision deficiency. The retina’s L, M, and S cones respond to overlapping wavelength ranges. When one cone type is absent or shifted, discriminating certain hue pairs becomes unreliable. Design and engineering tools do not simulate individual biology. They apply matrix transforms, fixed linear algebra on RGB values, that approximate how average observers with a given deficiency perceive those colors on average displays. Good enough for catching categorical collisions. Not a substitute for user research with people who have CVD.
| Type | Affected cones | Prevalence (approx.) | UI impact |
|---|---|---|---|
| Protanopia | L (red) absent | ~1% of males | Red-green confusion; reds darken toward brown/gray |
| Deuteranopia | M (green) absent | ~1% of males | Red-green confusion; most common in QA scenarios |
| Tritanopia | S (blue) absent | ~0.001% | Blue-yellow confusion; rare but relevant for charts |
Protanomaly and deuteranomaly are more common than the absolute forms. Simulation matrices usually model the extreme case because it produces clearer pass-fail signals. If protanopia fails, protanomaly often struggles too. Tritanopia simulation looks exotic until you ship a financial dashboard where positive is green, caution is yellow, and negative is blue, and all three converge to similar gray-violet on certain monitors.
Simulation is step two. Step one is objective contrast and non-color affordances. WCAG 2.2 AA contrast on all text, icons treated as text, and essential graphical objects. Use the contrast checker on real token pairs, not Figma approximations. Links distinguishable without hue alone through underline, icon, weight change, or persistent affordance visible in grayscale. Charts and status indicators never encode meaning with red-green only. Add shape, pattern, position, or text labels. Form errors pair color with message text and aria-invalid. Do not recolor the border alone. Focus indicators visible in high-contrast and simulated views.
If a flow fails grayscale, it will fail simulation. Grayscale is the fastest sanity check. Screenshot the page, desaturate in any editor, ask whether every state is still identifiable. In stakeholder meetings, project the grayscale screenshot first. If executives cannot identify success versus error states, simulation will not save the design. Fix semantics before debating hex values.
Tools, priority flows, and CI automation
Firefox Inspector exposes Accessibility panel simulation with protanopia, deuteranopia, tritanopia, and achromatopsia. It applies live to the rendered page. Best for walking interactive flows including hover states, focus rings, and toast notifications. Chrome Rendering tab via Command Palette Show Rendering offers emulate vision deficiencies. Useful alongside Lighthouse accessibility audits. Safari has no built-in CVD emulation as of current releases. Teams on Safari-only workflows should keep Firefox installed for color QA specifically.
DevTools simulation is free, zero setup, and tied to actual CSS including gradients, transparency, and blend modes. Use it on staging URLs, not Storybook in isolation, because production CSS order and token values are what matter. Figma plugins including Stark simulate on static frames. Valuable during palette selection before code exists. Weak for catching hover and dynamic chart colors. Design simulation should mirror token names headed for CSS. If Figma shows passing deuteranopia but engineering ships different hex from an outdated export, the simulation was theater.
Do not only check the marketing homepage. Priority flows include dashboards with status badges and heat map legends, forms with validation errors and required field markers, tables with row highlights and selected states, alerts and toasts showing success, warning, error, and info in one viewport, onboarding and empty states where muted text meets colored backgrounds, and dark mode where token swaps change contrast relationships simulation matrices still model. For each flow, capture four screenshots: normal, deuteranopia, protanopia, and tritanopia. Name files systematically. Attach to the ticket. Ask one question per screenshot: can a user complete the task without reading hue? If failed and pending rows trade places in deuteranopia, the fix is not adjust contrast. It is add an icon, label, or pattern.
Manual DevTools walks do not scale across dozens of PRs. Automate what machines measure well. Keep human review for meaning. Extract token pairs from CSS theme or root blocks. Compute WCAG contrast ratios in a Node script. Fail when text-muted on surface drops below 4.5:1, when accent on surface fails for button labels, or when dark mode overrides introduce pairs that passed in light mode only. Libraries include wcag-contrast, colorjs.io, or apca-w3 if your standard is APCA.
Screenshot simulation in CI can follow a pipeline where Playwright renders key routes at desktop and mobile breakpoints, post-processes screenshots with a CVD simulation library using Daltonization matrices, and diffs against baseline or runs structural checks. False positives happen when anti-aliasing shifts single pixels. Tune diff thresholds. Prefer structural checks over pixel identity. ESLint custom rules or stylelint plugins can flag class strings that only change text-red and text-green without accompanying aria, icon component, or data-status attribute in the same element subtree.
CI cannot catch whether a chart feels readable at a glance, brand approval of desaturated simulation appearance, or physical display variance including HDR, night shift, and broken calibration. CI is a gate, not a sign-off. Output artifacts should include contrast report JSON, simulated screenshot folder, and pass-fail summary in the PR comment.
Stakeholder review, fixing collisions, and when simulation lies
Designers, PMs, and legal care about different failure modes. A structured review prevents looks fine to me from closing tickets. Design systems owns token pairs, semantic color roles, and simulation baselines. QA executes flow checklist, attaches screenshots, and files defects. Product accepts or rejects discriminability risk for edge cases. An accessibility champion escalates WCAG versus best-practice gaps.
Review cadence spans palette phase with Figma simulation on component specs before handoff, pre-merge with developer-attached DevTools screenshots for touched flows in the PR template, pre-release with QA regression pack including CVD set compared to previous release baselines, and post-incident where user reports add flows to the permanent checklist. Store evidence packets in the ticket wiki or release folder with date and build hash, tool and deficiency type, standard claimed, screenshots normal plus three deficiencies, and known exceptions with product sign-off. When someone asks did we check accessibility, the answer is a link, not a memory.
Red-green status collisions fix with icons, text labels, or left-border stripes while keeping color as reinforcement. Pie charts need direct labels on slices, pattern fills, and size sorting so position carries meaning. Heat maps favor sequential luminance scales instead of red-green diverging scales plus numeric tooltips. Links in body text need underline plus color with visited distinct in luminance, not only hue. Placeholder text still counts as text under WCAG. Disabled buttons often fail contrast by intentional graying. If disabled state must communicate unavailable, ensure aria-disabled and tooltip. Do not rely on faint gray alone. Deeper contrast theory for typography thresholds lives in APCA and WCAG.
Simulation matrices are useful heuristics with known false friends. Semi-transparent reds over white backgrounds simulate differently than opaque reds. rgba error washes may vanish in deuteranopia while solid border passes contrast. Test composed pixels, not swatch tokens in isolation. Photographs and multi-stop gradients are not token-swappable. Simulation on photos is valid. Fixing may require art direction, not CSS. Video and animation can pulse through distinguishable states briefly while the animation average looks collapsed. Test paused frames and worst-case keyframes.
Wide-gamut P3 displays render more saturated reds and greens. Simulation on an sRGB export may understate collision on iPhones. macOS Night Shift and True Tone shift whites toward amber. Real users with mild deficiencies may perform better or worse than protanopia simulation suggests in specific pairs. Some tools adjust colors to be distinguishable through Daltonization rather than show how deficient vision sees them. Confirm your tool simulates deficiency. Do not ship Daltonized palettes as accessible fixes without user testing. When simulation failed but product insists on minimal UI, recruit participants with CVD through panels. Five sessions surface collisions matrices miss. Budget this for flagship flows including checkout, health dashboards, and security alerts.
Accessibility audits arrive years after launch. Evidence packets prove diligence. Update the packet when tokens change. Dark mode launches and rebrand both invalidate old screenshots. Color-vision QA reduces support tickets, prevents mis-clicks on financial and health UIs, and satisfies procurement questionnaires asking for WCAG conformance evidence. Simulation takes minutes per flow. Explaining a post-launch recall takes months.
Case study: the dashboard that passed contrast but failed meaning
A logistics SaaS company shipped a redesigned operations dashboard in March. The product used semantic status colors: failed deliveries in red at oklch(0.55 0.20 25), pending in amber at oklch(0.75 0.15 75), and completed in green at oklch(0.60 0.16 145). WCAG contrast checks passed for badge text on each fill and for row text on the default white surface. QA signed off on pixels. Marketing approved screenshots for the release blog. Engineering merged Friday afternoon. The release went live Sunday evening.
Monday morning a support ticket arrived from a warehouse manager in the Midwest. He could not tell which rows were failed versus pending. Both states read as the same muddy brown on his monitor. He had deuteranopia, the most common form of red-green confusion among men. The ticket included a phone photo of his laptop screen. Failed and pending badges were indistinguishable. He had been manually clicking into each row to read the status text, adding four minutes per review cycle across hundreds of daily shipments.
The support lead reproduced the issue in Firefox with deuteranopia simulation on staging, which still ran the previous release. The new dashboard was production only. QA pulled the production URL into Firefox Inspector, enabled deuteranopia simulation, and saw the collision immediately. Red and amber badges collapsed to similar brown-gray. Green and completed remained separable from both, but failed versus pending, the critical operational distinction, did not survive simulation. Grayscale screenshot of the same viewport showed the same collision. The palette had passed contrast. It had failed discriminability.
The incident response team convened Monday afternoon. Design systems pulled the token pairs and re-ran the contrast matrix. Every pair still passed 4.5:1. The failure mode was categorical, not luminance. Product insisted the color language matched industry conventions: red bad, amber waiting, green done. Engineering noted that industry convention does not satisfy WCAG 1.4.1 use of color when status is the only differentiator. The table rows encoded status through badge hue alone. No icon, no left border stripe, no text label inside the badge beyond the single word Failed or Pending in small caps that disappeared visually when hue collapsed.
The hotfix shipped Wednesday. Each badge gained an icon: X for failed, dash for pending, check for completed. Row left border gained a three-pixel stripe matching status for users who scanned vertically. Badge text remained. Color remained as reinforcement, not sole channel. QA captured four screenshots per flow: normal, deuteranopia, protanopia, tritanopia. Files attached to the incident ticket with naming convention dashboard-status-deuteranopia.png. Firefox 125 documented as simulation tool. Build hash recorded.
The post-incident review added the operations dashboard to the permanent CVD regression pack. CI contrast script already ran on token pairs. A new manual-required row in the spreadsheet flagged status badges for simulation screenshot review on every PR touching semantic color tokens. Design systems blocked export of red-green-only status components from Figma library until an icon slot was mandatory in the component spec. Product accepted one sprint slip to retrofit icons on two older screens that had shipped before the dashboard redesign with the same collision pattern.
Four weeks later procurement on an enterprise renewal asked for WCAG conformance evidence. Legal forwarded the incident evidence packet: grayscale veto screenshot from the Monday review, deuteranopia collision before fix, deuteranopia pass after fix, and contrast matrix CSV dated Wednesday hotfix. The renewal closed. The warehouse manager’s ticket closed with a personal reply from product. Support volume on status confusion dropped to zero in thirty days.
The lesson embedded in onboarding for new designers: contrast ratio is necessary and not sufficient. Run deuteranopia on every status color. Run protanopia on every red-accent CTA. Run tritanopia on every blue-yellow chart. Attach screenshots to the ticket. Ship when meaning survives, not when the palette looks beautiful on a calibrated monitor alone.