How do you keep contrast intact across both light and dark mode?

On this page

You keep contrast intact by verifying it separately in each theme, because a color pairing that passes in light mode can fail in dark and the reverse, so each mode has to be defined and checked on its own values rather than reused from the other. The method is to treat light and dark as two distinct designs that happen to share a structure, validating every text-on-background and component pairing independently. The shortcut that breaks this, reusing the same colors and just swapping the page background, fails because contrast is a relationship between two specific colors, and changing one side of the relationship changes the result.

The mechanism is in how contrast ratio works. Contrast is computed from the relative luminance of the foreground and the background, so a mid-gray that has comfortable contrast against white can be nearly invisible against near-black, and an accent blue that pops on a dark surface can wash out on a light one. The WCAG 1.4.3 thresholds you are validating against are the same in both themes (at Level AA, 4.5 to 1 for normal text, and 3 to 1 for large text and for the visual boundaries of user-interface components and meaningful graphics), but the colors feeding those ratios are different, so a pass in one mode tells you nothing about the other. You cannot inherit a result; you have to recompute it.

A concrete example: a brand uses a muted teal for links. On the light theme, teal on white measures comfortably above 4.5 to 1 and passes. The team builds dark mode by keeping the same teal and dropping the background to near-black, assuming the links are fine because they were fine before. But teal against near-black drops well under the threshold and the links become hard to read for low-vision users in the dark theme. The fix is a separate, lightened teal for dark mode, chosen and measured against the dark surface, so the link color is theme-specific rather than shared.

The deeper trap is “dark gray on darker gray,” the elegant low-contrast look that dark interfaces invite. Designers often build dark mode by layering near-black surfaces and placing soft gray text on them, which reads as sophisticated and quietly fails contrast across the whole theme. The discipline is to set up theme-specific design tokens, one value for each role in light and a deliberately chosen value for the same role in dark, and to run a contrast check on every pairing in both, including non-text elements: focus rings, input borders, icons, disabled states, and any color that carries meaning, since those carry their own 3 to 1 requirement.

One case sits outside this: this is about validating each theme, not about theming architecture or how you implement the switch. Tokens, CSS custom properties, and media queries are all fine plumbing, but none of them check contrast for you; they only make it easier to give each theme its own values. And do not over-correct in the other direction: pure white text on pure black is technically high contrast but can cause halation and eye strain for some readers, so aim for strong-but-comfortable values (slightly off-white on a dark-gray surface, for instance) that still clear the thresholds.

When you build a themeable interface, define independent color values for light and dark, then run every text and component pairing through a contrast check in each mode rather than assuming a passing light theme carries over. Verify both themes before shipping, not just the one you designed first, and recheck whenever a brand color changes, because in two themes one color does two different jobs and only measuring each one tells you whether both are readable.

Leave a comment

Your email address will not be published. Required fields are marked *