Skip to content
AtomicReps

The pair passed and nobody can read it

A lesson from UI and UX for Engineers: Decisions You Made By Accident. Play it above, or read it through below.

  • The ratio is a light meter. It was never a reader.

    The common read: The contrast ratio tracks how readable a pair is, so against a mid grey the two obvious text colours score about the same.

    The number measures light, and reading was never one of its inputs. The formula is a ratio of relative luminance, which is the light a display emits, and the sRGB transfer curve puts far more of the hex range near the dark end than a linear reading suggests. That is why #a3a3a3 resolves to 0.3663 rather than to a half, and why the two candidates on the previous screen came out a factor of three apart.

    Two properties follow from the definition itself, and both surprise people who have used the number for years. It is symmetric: the specification takes the lighter colour as L1 and the darker as L2, so swapping the text and the background cannot change the result, and the metric has no idea which of the two you are setting type in. And hue is deliberately absent. The Understanding document says so directly, and gives the reason.

    The W3C also fixed the colour space and then stopped. The luminance definition is written for sRGB, with a note advising authors to evaluate in sRGB unless they know another space is in play, and there is no normative treatment of display-p3, oklch or rec2020 anywhere in WCAG 2.2. Tailwind moved its whole default palette to oklch in January 2025. Two tools gamut-mapping that palette differently will report two different ratios for one token, and nothing in the specification says which is right.

    That arrives as "the contrast check is flaky, it passed locally and failed in CI", filed against the audit tool, then against the CI cache, and closed by pinning a version. Nothing was flaky. The input was outside the space the formula is defined for. Read the number as what it is, a photometric comparison with a conformance threshold bolted on, and price it honestly: it will tell you when a pair is definitely too close, and it will not tell you that a pair is comfortable to read.

  • There is no successor. There is a draft that says so.

    The common read: APCA is the contrast algorithm WCAG 3 adopted, so the ratio I am using now is a legacy metric with a dated replacement.

    The current draft defines no contrast algorithm, and it says so in an editor's note rather than in a footnote. Its contrast entry sits at Exploratory status, the note under it states that the algorithm is yet to be determined, and the document's own boilerplate says it is inappropriate to cite it as anything other than work in progress. No regulation anywhere that this pack could find references APCA: not WCAG 2.2, not EN 301 549 v3.2.1, not the United States Department of Justice rule for Title II.

    APCA itself is real work and is worth knowing about, and the honest summary of it is narrower than either side usually gives. It reports a signed lightness contrast, Lc, so it knows which colour is the text where the 2.x ratio cannot; its published thresholds take font size and weight as inputs, which the ratio does not; and the divergence between the two is demonstrable rather than argued.

    What is not demonstrated is that it predicts real reading better for real people on real screens. Its most sympathetic independent reviewer built the interactive comparison that everyone cites, published reproducible numbers, and closed by asking for the study that would settle it. Treat that as the state of the evidence: the disagreement between the two algorithms is a fact, and the ranking between them is an open question.

    That arrives as a roadmap line nobody can close, filed as "migrate design tokens to APCA", carried across three quarters, and deleted after a procurement questionnaire asks which version of which standard the product conforms to. Ship against the standard that exists: keep the conformance claim on WCAG 2.x ratios and any Lc figure as supplementary evidence with no normative weight. If you wire APCA into a lint, pin the reference implementation rather than porting its constants, which move between revisions.

  • A threshold does not round, and it has four exceptions you have not read.

    The common read: 4.5:1 is the accessibility bar, so a checker reporting 4.5 means the text is fine and the work is done.

    Which of the two greys on the previous screen was the wrong one to ship? Neither answer is about the number. #767676 cleared by four hundredths and #777777 missed by two, and the Understanding document is explicit that the computed value is not rounded, so 4.499:1 does not meet a 4.5:1 threshold. A pass at 4.51 and a fail at 4.48 describe the same text.

    The criterion is also narrower than its reputation, and the carve-outs are where products actually live. Large-scale text drops to 3:1, and the definition is in POINTS: at least 18 point, or 14 point bold. The W3C's own Understanding text converts those to approximately 18.5px and 24px. The 18.66px that appears in nearly every blog post and in several tool interfaces is somebody's arithmetic, not a number the W3C printed, and neither figure is normative because the normative unit is points.

    Three more exceptions ride the same criterion: incidental text, logotypes, and text that is part of an inactive user interface component. That last one is load-bearing and it is undefined. The string "inactive" appears three times in the whole of WCAG 2.2, all three inside criteria, and there is no glossary entry for it. Whether a control carrying aria-disabled="true" with its click handler still attached counts as inactive is your judgement, not the specification's.

    That arrives as a row in a 400-line audit spreadsheet reading "Settings > muted label > 4.47:1 > FAIL", filed against the design system, escalated because the token is approved, and closed by darkening it for the whole product. Treat the number as a floor with a hard edge, not a grade. Check the states nobody checks, which the Understanding document names as placeholder, hover and focus text, and take the honest limit: the criterion has no opinion about whether your 14px grey is comfortable.

  • The formula is generous exactly where you stopped checking.

    The common read: The contrast formula penalises dark backgrounds, so a dark theme has to work harder than a light one to reach the same number.

    The formula does not punish dark themes. It flatters them, which is worse, because a flattering number is one nobody re-checks. The criticism that made this famous sits on a W3C wiki page and states the direction plainly: the formula is not perceptual and fails to predict contrast appropriately if the background is darker than about #aaa. The +0.05 term models screen flare, and its effect is largest where both luminances are small, which is the whole dark end of a dark theme.

    Two claims about dark mode are worth separating, because the strong one is folklore and the weak one is real. The white-text-on-black halation argument that everyone repeats traces to a single attributed blog comment from around 2004, and the source pack found no peer-reviewed study behind it. What the literature does support is a general polarity effect: dark text on a light background produces measurably better reading performance for people with typical vision, with pupil size as the reported mechanism.

    So dark mode is a preference and an accommodation rather than an accessibility upgrade. It is a genuine accommodation, with reading-rate evidence behind it, for people with cloudy ocular media, and a genuine preference for everyone else. What it is not is a reason to skip the audit, and the flattering-number effect means the dark theme is the one where a green result carries less information.

    That arrives as "dark mode is hard to read", filed with no reproduction steps, closed as not reproducible by three people whose monitors are brighter than the reporter's, and reopened every quarter. Audit both themes as separate products, and take the honest consequence of the paragraph above: a dark pair that clears 4.5:1 has cleared a bar the specification's own critics say is set too low there, so spend the extra contrast in the dark theme rather than banking it.

  • The browser can take the palette away.

    The common read: The colours in my stylesheet are the colours on the user's screen, so auditing my palette audits what people see.

    Which of your colours survives a Windows contrast theme? The honest answer for most products is none of them. In forced-colors mode the browser treats color, background-color, border-color and outline-color as if no author value were specified, forces box-shadow and text-shadow to none, and forces non-url background-image to none. Every focus ring built from a box shadow disappears. Every status dot built from a background colour collapses into the system palette.

    The legacy hook for this is gone rather than deprecated. Edge removed -ms-high-contrast and -ms-high-contrast-adjust completely in version 138, June 2025, so any block in a mature codebase keyed on that media feature is dead CSS. The replacement is forced-colors, and it is a different mechanism: it does not ask you to restate your palette, it removes it.

    Three more places the palette is not yours. Without color-scheme: light dark the browser still paints a light canvas, light scrollbars and light form controls under a dark page, and light-dark() silently returns its first value with no error and no warning. Chrome on Android can generate its own dark theme over a light site, and the documented opt-out is color-scheme: only light. And contrast-color(), newly available since April 2026, returns black or white and ships with a warning from its own documentation.

    That warning is a CSS function telling you the standard it implements is not enough: mid-tone backgrounds do not provide enough contrast with either black or white, and MDN gives #2277d3 as the failing example. Measured this session, that blue is 4.65:1 against black and 4.52:1 against white, so both directions clear AA and neither is readable for small text. Audit the modes rather than the palette: light, dark, forced colors, and prefers-contrast: less, which almost nobody handles.