skip to content

After dark mode shipped in a recruiting app, the rejected-stage chip failed contrast only in dark. How would you keep every mode tested across the component library?

level: seniorimportance: must knowfreq 42%

answer

  1. what was checked, and in which mode
  2. declare the color pairs as data
  3. compute every pair per mode
  4. render components per mode and state
  5. mode is a review dimension

basics

~20 s

Declare every foreground-background pair as data and compute its contrast in every mode on each build, failing below threshold. Add visual tests of components in each mode and key state, and make every mode part of review and release readiness.

solid answer

~50 s

The chip failed because contrast was verified only in the light mode: the dark mapping of the rejected text on the rejected surface was never computed. Fix the process, not just the chip. First, **declare the color pairs** the system intends — text on each surface, status text on status surfaces, icons, borders and focus indicators on their backgrounds — and have the token build **compute every pair in every mode**, failing below 4.5:1 for normal text and 3:1 for component and state indicators under WCAG 2.2 Level AA. Second, render each component in the workshop **in every mode and key state** — hover, selected, focus, disabled — with visual baselines per mode, because pair checks miss translucency and real layering. Third, make mode a **review dimension**: every component change is shown in all modes, and a component is not release-ready until each mode passes.

code

pseudocode · 13 lines
pseudocode
pairs = [
  ("text.primary",          "surface.default",          TEXT),
  ("status.rejected.text",  "status.rejected.surface",  TEXT),
  ("border.input",          "surface.default",          NON_TEXT),
  ("focus.indicator",       "surface.raised",           NON_TEXT)
]
minimum = { TEXT: 4.5, NON_TEXT: 3.0 }

for mode in [light, dark, light_high_contrast, dark_high_contrast]:
  for (fg, bg, kind) in pairs:
    ratio = contrast(resolve(fg, mode), resolve(bg, mode))
    if ratio < minimum[kind]:
      fail(mode, fg, bg, ratio)

go deeper

for a junior

Recall that each mode has its own colors, so contrast has to be checked separately in light, dark and any high-contrast mode.

for a middle

Explain declared color pairs computed per mode in the build, why states count as pairs, and what pair checks cannot see.

for a senior

Design the three layers — per-mode pair checks, per-mode visual tests, scans of real screens — and write mode into review and release readiness so no mode ships unchecked.

for a principal

Decide how much per-mode test depth each component tier gets, balancing matrix cost against where contrast failures actually hurt users.

## What went wrong A recruiting app shipped dark mode. A week later, a recruiter reported that the *Rejected* chip on the candidate pipeline was nearly unreadable in dark mode. The light version passed every check. The dark version had never been checked: the team had remapped `status.rejected.text` and `status.rejected.surface` for dark, but contrast was only ever measured on the light values that designers had originally reviewed. The lesson is structural. Once a system has several modes, every visual guarantee has to be verified **per mode**, or the guarantees silently cover only the mode people look at most. ## Layer 1: pair checks on tokens, every mode, every build The cheapest and most exhaustive check runs before any component renders: 1. **Declare the intended pairs as data** — `text.primary` on `surface.default`, `text.secondary` on `surface.raised`, each `status.*.text` on its `status.*.surface`, icons and borders on their backgrounds, the focus indicator on every surface it can appear over. 2. **Resolve each pair in every mode** — light, dark, and each high-contrast set. 3. **Compute the contrast ratio** and fail the build below threshold: under WCAG 2.2 Level AA, 4.5:1 for normal text (1.4.3) and 3:1 for component boundaries and state indicators (1.4.11). 4. **Include state pairs.** Hover, selected and focused colors are pairs too; the Understanding document for 1.4.11 tells testers to check the contrast indicators in each state. This catches the rejected-chip bug on the day the dark value was written. ## Layer 2: visual tests per mode and state Pair checks see tokens, not screens. They miss: - **translucent layers** — a semi-transparent overlay whose effective color depends on what lies beneath; - **text over images**, such as a candidate's name over a cover photo; - **a component using the wrong token** for its context, which no pair list anticipates. So each component in the workshop is rendered **in every mode and in its key states** — default, hover, selected, focused, disabled, error — and compared against a baseline per mode. The matrix grows as components × modes × states, so prioritise: every state for the components that carry status and focus, fewer for purely structural ones. ## Layer 3: automated scans of real screens An automated accessibility scanner run over the real pipeline, candidate profile and scheduling screens in each mode catches combinations that only occur in composed pages. It does not replace the first two layers; it covers what they cannot see. ## Making it habitual | Practice | What it prevents | |---|---| | Every mode shown in the component workshop by default | Reviewers looking only at light | | Mode completeness check: every semantic token has a value in every mode | A token silently keeping its light value in dark | | Release-readiness bar lists each supported mode | Components shipped light-only | | Change reviews show before-and-after in all modes | Value changes breaking one mode unnoticed | | A new mode ships with its pairs declared and passing | Adding a mode without verification | ## Beyond ratios Not everything is a number. A color-replacement contrast mode needs a human look at whether boundaries, selection and focus are still visible, and a dark mode needs a look at glare from saturated colors. Treat those as scheduled reviews of key screens rather than one-off launch checks. The interview answer in one line: verify guarantees per mode — declared pairs computed in every mode, components rendered in every mode and state, and mode written into review and release readiness — so the next mode cannot ship with only half its checks.

  • Why do the pair checks need to include hover, selected and focused colors, not just defaults?
    State colors are separate values in each mode, and a state indicator that loses contrast is as much a failure as default text. The Understanding document for Non-text Contrast tells testers to check indicators in each state. A dark-mode focus ring that nearly matches the raised surface passes every default-state check and still hides focus.
  • The visual test matrix is getting too large. How do you cut it without losing the protection?
    Keep the token pair checks exhaustive, since they are cheap. For rendered tests, cover every mode for every component in its default state, and every state only for components that carry status, selection or focus. Then add a small set of composed screens per mode. The expensive combinations go where contrast failures actually hurt.

saying these in an interview costs you the question

  • If the light mode passes contrast, the dark mapping will pass too.
  • Checking token pairs is enough; rendered visual tests per mode are redundant.
  • Only default states need contrast checks; hover and focus are optional.
  • Every component must be tested in every mode and every state with equal depth.
  • Contrast checks run once at launch; later value changes do not need them.