CategoryProduct design
Dark mode is not inverted color: contrast in practice
Dark mode isn’t color inversion. It is a second choice of the contrast scale, built from the same tokens with different values. A theme produced by flipping the light values keeps the old hierarchy and loses the separation between layers. That is why automatic inversion tends to fail WCAG 2.2.
Why inversion breaks the hierarchy
A contrast ratio measures a relation between two colors. It doesn’t measure the place of a color in the interface.
In a light theme the page is the lightest surface. A card sits above it with a slightly different value, and a dialog sits above the card. The order of the surfaces carries the meaning.
Flipping the values mirrors that order. The card that read as raised now reads as recessed, because the surface behind it is darker than the surface itself. The ratio between the two colors can stay the same while the meaning of the pair changes.
Hierarchy is a relation between surfaces. Inversion moves values, not relations.
What WCAG 2.2 requires
The Web Content Accessibility Guidelines version 2.2 state three thresholds for contrast. Each one applies to a different kind of content.
- Success Criterion 1.4.3, level AA, requires a contrast ratio of at least 4.5 to 1 for text and images of text.
- The same criterion lowers the requirement to 3 to 1 for large-scale text. The glossary defines large-scale as at least 18 point, or 14 point bold.
- Success Criterion 1.4.11, level AA, requires 3 to 1 against adjacent colors for user interface components and graphical objects.
- Success Criterion 1.4.6, level AAA, raises the text requirement to 7 to 1 and keeps 4.5 to 1 for large text.
The Understanding document for 1.4.3 gives the conversion. One point equals 1.333 CSS pixels, so 14 point is about 18.5 pixels and 18 point is about 24 pixels.
Two details decide most failures. The thresholds aren’t rounded, and 4.499 to 1 doesn’t meet 4.5 to 1. And a component with no text and no icon needs a visible boundary at 3 to 1, since the boundary is then the only sign that the control exists (WCAG 1.4.3, WCAG 1.4.11).
How to measure contrast
The contrast ratio is (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter color and L2 is the relative luminance of the darker one. The glossary notes that the result ranges from 1 to 1 up to 21 to 1.
Luminance isn’t the average of the RGB channels. A tool that averages channels reports a number that WCAG doesn’t use.
The Understanding document asks the author to read the colors from the markup or the stylesheets, not from the rendered screen. Anti-aliasing makes thin text and thin lines render fainter than the declared color.
That note carries a practical warning. A thin font can pass the calculation and still read poorly. The document recommends a heavier font, or a pair of colors that exceeds the threshold.
Behind text on a gradient or a photograph, the guidance is to test the least contrasting area, not the average. The color scheme of the Material Design guide and the contrast inside it are the subject of color and the color role.
Why shadow doesn’t separate layers in the dark
A shadow works by making a surface darker than the surface behind it. On a dark background there is little room left to go darker, so the shadow loses its job.
The value of the background itself does that job instead. In a dark theme a layer sits above the page when its background is lighter than the page, and below when it is darker. Elevation becomes a scale of lightness instead of a scale of shadow.
That scale has a limit. Near black, two steps of lightness are hard to tell apart, so a theme that starts at pure black runs out of room after one or two layers.
Carbon, IBM’s design system, documents this as a layering model. Its themes pair a light theme, White or Gray 10, with a dark theme, Gray 100 or Gray 90. The layers are named by position, not by color value.
Borders and outlines carry part of the work as well. In the dark, a hairline border at 3 to 1 against both surfaces replaces the shadow that separated the card in the light theme (WCAG 1.4.11).
Color token against raw value
A color token names a role. A raw value names a color. The difference decides how much work a theme change costs.
Carbon states the rule in one line. You can’t implement light or dark mode without using color tokens everywhere in your product, and hard-coded values don’t change when the mode switches.
A token also carries the verification with it. When the button background is a token, the build reads the pair of tokens and computes the ratio. When the background is a literal, the check needs a human eye and a screenshot.
The practical test is short. Search the styles for hex values outside the token file. Each one found is a place where the dark theme can fail in silence.
Tokens need names that survive the theme switch. A token called light-gray stops meaning anything in the dark theme. A token called surface-raised keeps its meaning in both.
How to test contrast in the build
A contrast check belongs in the build, next to the lint and the type check. Manual review catches the screens that someone remembered to open.
The check needs three inputs. It needs the token pairs in use, the background under each text color, and the size and weight of the text. The size decides which threshold applies, since large text drops from 4.5 to 1 to 3 to 1 (WCAG 1.4.3).
The result is a list, not a yes or no. The list should name the token pair, the computed ratio, the threshold and the file that uses the pair.
Two rules keep the check accurate. Don’t round the ratio before comparing it, because the guidance treats 4.499 to 1 as a failure. And resolve the real background, because text over a card over a page has two candidates and the stricter one wins (WCAG 1.4.3).
A check that reads tokens covers the theme switch. A check that reads a screenshot covers one theme and one screen size.
The tooling is secondary. Any script that converts the token values to luminance and applies the formula works, and the formula is public.
Focus and disabled states, where contrast escapes
Focus is the state where contrast is lost most often, because the indicator lives on the boundary between two surfaces.
Success Criterion 1.4.11 covers it. Any visual information needed to indicate state, such as selected or focused, must meet 3 to 1 against the adjacent colors (WCAG 1.4.11).
Success Criterion 2.4.13, level AAA, adds a measure. The focus indicator must cover at least the area of a 2-CSS-pixel-thick perimeter of the unfocused component, and reach 3 to 1 between the focused and unfocused states.
The adjacent colors rule decides the hard case. A focus ring inside a card must contrast with the card, and a ring outside must contrast with the page. A ring drawn on the boundary can fail both.
The disabled state is the other escape, and it comes with a written exception. Both 1.4.3 and 1.4.11 exempt inactive user interface components, so a grayed-out control has no contrast requirement.
That exception isn’t a design instruction. A disabled control that can’t be read leaves the person unable to tell which action is blocked, and the Understanding document notes that low-contrast controls may be missed entirely.
A rule that works is to dim the disabled control less than your instinct says, and to keep the label above the ratio of a body text. The exception removes the obligation, not the cost.
Where dark mode is worse, and when light wins
Dark mode doesn’t come for free. Its costs are documented.
Carbon records that dark mode is better for people with cataract and related disorders, and that it emits less light, which can reduce eye strain and headaches. The same page notes that some research shows unimpaired sighted users perform better in light mode.
Long text is the weak point. A page of body copy in a light color on a dark background is harder to read than the same text in the light theme. A thin font weight makes it worse.
The light theme is the right choice for long reading, for documentation, for forms with many fields, and for any surface a person stares at for an hour. Dark mode fits short sessions, media, code, and interfaces used at night.
Often the best answer isn’t to choose. When the two themes share the token set and both pass the check, the person decides by preference and the build protects both sides.
Where Nielsen Norman Group disagrees
The three Nielsen Norman Group articles don’t tell the same story, and it is worth showing where they part ways.
The article on dark mode reviews the literature and reports that negative polarity hurts mainly people with normal vision. The article on the problems reported by dark mode users goes further in a different direction. It says the effect is much smaller than the claims suggest. Part of the research found no significant difference in eye strain or headache, and ambient light mattered as much as the color. The two articles measure the same theme and land on different sizes of effect.
The other two articles aren’t about a mode at all. The article on low contrast is from 2015 and asks for a larger font before lower contrast. The article on legibility asks for high contrast between the character and the background. They converge against washed-out text, including the washed-out text of a light theme.
The position in this post is the narrower one. It keeps the ratio as the testable bar and treats the mode as a second scale, and it doesn’t claim that one polarity wins for everyone.
Sources
- W3C, Understanding Success Criterion 1.4.3: Contrast (Minimum)
- W3C, Understanding Success Criterion 1.4.6: Contrast (Enhanced)
- W3C, Understanding Success Criterion 1.4.11: Non-text Contrast
- W3C, Understanding Success Criterion 2.4.13: Focus Appearance
- W3C, Web Content Accessibility Guidelines (WCAG) 2.2
- IBM, Carbon Design System, Color usage
- Nielsen Norman Group, Dark Mode
- Nielsen Norman Group, Dark Mode Users’ Issues
- Nielsen Norman Group, Low-Contrast Text
- Nielsen Norman Group, Legibility, Readability, and Comprehension
How to verify before publishing
- Compute the ratio for every text token pair and compare it to the threshold for its size, without rounding.
- Check every focus ring against both adjacent surfaces, inside the card and outside it.
- Read the disabled control at arm’s length and decide whether the label still says which action is blocked.
- Confirm that no shadow carries separation alone, and that a border or a step of lightness replaces it.
- Search the styles for hex values that bypass the token file, and move each one into a token.
- Switch the theme with the check running, and confirm the two themes report different values for the same tokens.
- Test the least contrasting area of any gradient or image behind text, not the average.
- Run the check in the build, so a new hard coded color fails before it reaches a person.
A dark theme that passes the same check as the light theme is a second scale, not a filter. Build the scale from tokens, measure it in the build, and let the person pick the side.


