CORE JSC

International Technology Partnership

UI/UX

Fixing Insufficient Color Contrast in Dark Mode That Passes Light Mode Accessibility Audits

The design system passes every WCAG contrast check in light mode, using the exact same color tokens dark mode reuses. Yet secondary text that reads clearly on white looks washed out and barely legible on a dark background — because contrast is a function of actual luminance, not a value that survives a simple invert or dim.

Core JSC Team·August 23, 2026
UI/UXDark ModeAccessibilityWCAGColor Contrast

The Problem

A design system's light-mode color tokens pass automated WCAG contrast audits without issue — body text, secondary/muted text, links, all meet the required ratios against a white or light background. Dark mode reuses the same design tokens, or a simple derived transform of them (inverted lightness, reduced opacity), and the same audit that passed cleanly in light mode now fails, or worse, was never actually run against dark mode at all. In practice, secondary text and captions that were perfectly legible in light mode become genuinely hard to read against the dark background.

Why It Happens

Contrast ratio is a nonlinear function of actual luminance, not a "visual distance" a naive transform preserves

The WCAG contrast formula compares the relative luminance of foreground and background colors — a specific, nonlinear calculation, not a rough sense of how different two colors look. A mid-gray that provides ample contrast against white doesn't automatically provide an equivalent ratio against a dark background, because the luminance math doesn't scale the way a simple mental model of "the same gray, just inverted" would suggest. A color that happened to pass in light mode by virtue of its specific luminance value can genuinely fail once the background luminance changes, even if nothing about the color itself was touched.

Dark mode is often built as a blanket transform of light mode rather than its own set of checked colors

A common shortcut is generating dark mode by applying a single transformation to every light-mode color — inverting lightness, reducing opacity by a fixed percentage — rather than defining dark-mode-specific tokens and independently verifying each one's contrast against its actual dark-mode background. This transform can happen to work for colors with generous contrast margin to begin with, and quietly fail for colors that were already close to the minimum threshold in light mode, since a blanket percentage shift doesn't account for where each individual color started.

A passing WCAG ratio doesn't fully capture real-world legibility, especially with pure black

Pure black (#000000) backgrounds paired with pure or near-pure white text can produce a WCAG ratio that technically passes while still causing real legibility issues for some users — a halation effect where high-contrast edges appear to "glow" or blur, particularly noticeable on OLED screens or for users with certain visual sensitivities. This is a real-world readability factor an automated contrast-ratio checker alone doesn't flag, since the number itself is fine.

The Fix

1. Define dark-mode-specific color tokens rather than deriving them with a blanket transform

:root {
  --text-secondary: #6b7280; /* checked against light background */
}
:root[data-theme="dark"] {
  --text-secondary: #9ca3af; /* independently checked against dark background — not a mechanical invert */
}

Each theme's colors should be chosen and verified on their own terms against their own background, rather than assuming a single transformation formula reliably preserves contrast across both themes for every token.

2. Run contrast checks against the actual rendered dark-mode output, not just the default theme

Most automated accessibility checks in CI run against whatever theme renders by default, which is very often light mode — extend that check to explicitly render and audit the dark-mode variant too, since a design system's dark theme is a distinct set of color pairings that needs its own verification, not an assumption that passing light mode implies passing dark mode.

3. Avoid pure black paired with pure white; use near-black and softened white instead

:root[data-theme="dark"] {
  --background: #0d0d0f; /* near-black rather than #000000 */
  --text-primary: #f2f2f3; /* softened white rather than #ffffff */
}

A slightly reduced luminance range still comfortably clears WCAG contrast minimums while reducing the halation and glare some users experience with true black-and-white extremes — this is a legibility improvement the raw contrast ratio number won't itself surface as a problem.

4. Manually spot-check secondary and muted text specifically, not just primary body text

Secondary text, captions, placeholder text, and disabled-state colors are typically the ones with the smallest contrast margin to begin with in any design system, since they're intentionally deprioritized visually — which makes them the first to actually fail when a theme transform or a reused token doesn't hold up, and the ones most worth checking by eye in addition to automated tooling.

Why This Works

Each fix addresses the same root issue: contrast is a property of a specific foreground-background pairing's actual luminance, not something that survives a blanket transformation applied uniformly across every color. Defining and independently verifying dark-mode-specific tokens respects that; auditing the dark theme explicitly in CI catches what a light-mode-only check structurally cannot; and moving away from pure black-and-white extremes addresses a real legibility factor that exists alongside, not instead of, the formal WCAG ratio.

Conclusion

A design system passing every accessibility check in light mode says nothing about dark mode's contrast, because dark mode is frequently built as a mechanical transform of light-mode colors rather than its own independently verified set — and the underlying contrast math doesn't preserve ratios across that kind of transform. Define and check dark-mode color tokens independently rather than deriving them from a blanket invert or opacity shift, run contrast audits against the actual rendered dark theme in CI, avoid pure black-and-white extremes in favor of near-black and softened white, and manually verify secondary and muted text specifically, since that's where contrast margins are thinnest to begin with.