Hypothesis
A design agent that gates dark-mode palettes on WCAG 2.x’s single 4.5:1 ratio will pass text that humans can’t comfortably read — because the ratio is polarity-blind, and APCA’s signed Lc is the fix. Our own muted footer gray, #8B8B8B on #1A1A1A, computes to Lc −39, below the Lc 45 floor for even spot-reading text: this blog fails its own lesson.
That failure matters beyond this site. The WebAIM Million 2026 found text below the WCAG 2 AA contrast thresholds on 83.9% of home pages — up from 79.1% in 2025 — making low-contrast text the most commonly-detected accessibility issue. The same APCA overview argues the WCAG 2.x math itself is part of the problem: its own citations report that studies show the ratio can pass colors that should fail and fail colors that should pass. This test asks whether an agent can compute a more useful contrast signal—and where the results diverge.
The issue connects to our earlier dark-mode blind spot analysis: a palette can pass a simple rule and still be a poor choice for secondary text.
Method
We computed contrast for a corpus of 15 pairs drawn from real design-system tokens, including Tailwind slate, zinc, and gray scales, Chakra gray.500, this blog’s grays, and agreement controls. WCAG 2.x ratios and APCA Lc values were calculated for the same color pairs, so the comparison isolates how the methods judge the supplied foreground and background colors. The table in the next section reports the 11 pairs where the two methods disagree most instructively; the remaining 4 are agreement controls listed with them.
The implementation is a Python transcription of APCAcontrast() from [email protected] (W3-licensed 0.0.98G-4g constants); it reproduced all 8 vectors in the package’s own test suite bit-exact; validated 2026-10-08. The apca-w3 package publishes the implementation and test suite; the algorithm repository documents the underlying method.
Every number in this post was computed by a small Python script from the published algorithm constants — no human screen testing was involved.
Token provenance comes from Tailwind’s color documentation and the Chakra UI v2 default theme. The table below reproduces the measured values verbatim.
Results
The computed pairs show two kinds of disagreement: WCAG passes that APCA flags, and WCAG failures that APCA passes. That split is not evenly distributed across light and dark backgrounds. The central result is POLARITY ASYMMETRY: the disagreement clusters by text polarity, putting dark-mode secondary text directly in the risk zone. Verdicts use the body-text floor (Lc 60) for fluent reading and the Lc 45 spot-text floor for the weakest acceptable role, per the threshold table in The Fix below.
| Pair | WCAG 2.x | APCA Lc | Verdict |
|---|---|---|---|
| #94A3B8 on #0F172A (slate-400/slate-900) | 6.96:1 PASS | −51 | divergent — passes spot floor 45, fails body 60 |
| #A1A1AA on #18181B (zinc-400/zinc-900) | 6.91:1 PASS | −51 | divergent — passes spot floor, fails body 60 |
| #9CA3AF on #111827 (gray-400/gray-900) | 6.99:1 PASS | −51 | divergent — passes spot floor, fails body 60 |
| #8B8B8B on #1A1A1A (this blog’s muted gray) | 5.11:1 PASS | −39 | divergent — fails both floors |
| #718096 on #FFFFFF (Chakra gray.500) | 4.02:1 FAIL | +67 | reverse — fails WCAG AA, passes APCA body 60 |
| #8C8C8C on #FFFFFF | 3.36:1 FAIL | +61 | reverse — passes APCA body 60 |
| #767676 on #FFFFFF | 4.54:1 | 72 | agree |
| #6B7280 on #FFFFFF (Tailwind gray-500) | 4.83:1 | 74 | agree |
| #FFFFFF on #0F172A (white on slate-900) | 17.85:1 | −107 | agree |
| #000000 on #FFFFFF | 21.0:1 | 106 | agree |
| #B8422E on #FFFFFF (terracotta) | 5.44:1 | 76 | agree |
All four WCAG-passes-APCA-flags pairs are light text on dark; both WCAG-fails-APCA-passes pairs are dark text on light. That is systematic bias, not noise—and it points exactly at dark-mode secondary text. It also gives a useful contrast to our earlier OKLCH perceptual color agent rule: perceptual color tools and contrast gates answer different questions.
Why the Ratio Breaks in Dark Mode
WCAG 2.x computes an unsigned ratio from relative luminance, adding a flat +0.05 to both luminance terms. On near-black backgrounds, that offset is large relative to the background luminance and dominates the denominator. The result can make a dark-mode pair look numerically sufficient even when the text remains too dim to read comfortably.
The ratio also has no idea which color is text and which is background. Reversing foreground and background leaves one unsigned contrast number, with no polarity signal to tell an agent whether it is judging light-on-dark or dark-on-light. MDN’s color contrast guidance explains the WCAG ratio approach; the APCA easy introduction describes why a polarity-aware model can better represent text contrast.
APCA instead returns a signed Lc value: negative for light text on dark and positive for dark text on light in these measurements. Its calculation uses a soft black clamp rather than the hard offset, then applies role- and polarity-dependent exponents. The relevant published constants include:
blkThrs 0.022
blkClmp 1.414
normBG 0.56
normTXT 0.57
revTXT 0.62
revBG 0.65
scale 1.14
low-clip 0.1
The scale runs roughly ±108. The sign carries polarity; the magnitude communicates contrast strength. Note the three −51 pairs: they clear the Lc 45 spot-text floor but fall short of the Lc 60 fluent-reading target, while the −39 pair misses both. The reverse cases on white (+61, +67) clear the body floor.
The Fix: A Polarity-Aware Agent Gate
An agent should retain the signed Lc result, use its magnitude to check a role-based floor, and preserve the sign as a polarity signal. That replaces one universal ratio with thresholds suited to how text is used. It also lets a linter identify dark-mode gray tokens for review instead of treating every passing ratio as equivalent.
def contrast_gate(fg_y, bg_y, role):
lc = APCAcontrast(fg_y, bg_y) # signed Lc
thresholds = {"body": 60, "spot": 45, "headline": 45}
return abs(lc) >= thresholds[role]
These are practical agent-gate thresholds, not a claim that computation alone guarantees comfortable reading:
| Text role | Suggested APCA threshold |
|---|---|
| Body text | Lc 60+; 75 = gold standard |
| Spot-reading / placeholder | Lc 45+ |
| Large headline | Lc 45+ |
The sign tells the agent which polarity it is evaluating; the magnitude replaces the single 4.5:1 gate. An implementation should keep the signed result in its report rather than returning only pass or fail. That makes polarity-specific palette issues visible to designers and downstream checks.
Contrast is one computable design property among others. Our CRAP framework for agent-computable design and TypeSpy typography rubric address additional signals an agent can evaluate.
WCAG 2.x vs APCA at a Glance
WCAG 2.x and APCA produce different kinds of output, use different threshold models, and carry different standards status. An agent should therefore treat them as complementary checks rather than interchangeable formulas: one supports conformance evaluation today, while the other adds a role-aware, polarity-aware reading signal.
| Dimension | WCAG 2.x | APCA |
|---|---|---|
| Output | Unsigned ratio | Signed Lc |
| Polarity awareness | None | Built-in |
| Thresholds | Flat 4.5:1 / 3:1 | Role-based 60 / 45 |
| Dark-mode behavior | Inflated by the +0.05 offset | Polarity-dependent (separate reverse-polarity exponents) |
| Standards status | Normative since WCAG 2.0 | Not yet referenced by any W3C draft; WCAG 3’s algorithm is undetermined |
For background on how contrast is evaluated in the current guidance, see MDN’s overview. The distinction is practical: a ratio is useful for checking current criteria, but its pass status does not reveal APCA’s polarity-sensitive warning.
Self-Audit: Our Muted Gray Fails
Our own muted footer gray, #8B8B8B on #1A1A1A, scores WCAG 5.11:1 and passes that ratio gate. Its APCA Lc is −39, below even the Lc 45 spot-text floor. The result is a concrete example of how an agent using only WCAG can accept a dark-mode secondary color that an APCA-informed check would flag.
A fix would mean raising muted grays toward Lc 45–60, then checking the surrounding palette and text role. That is a proposed correction, not a claim that we have shipped one. The same principle applies to other interface checks: our forced-colors blind spot shows why a single successful computation cannot stand in for every accessibility condition.
Status Check: Can You Ship APCA?
APCA is frequently described as the likely method for WCAG 3, but the current WCAG 3 draft (Working Draft, September 2026) does not name it: an editor’s note states “the contrast algorithm used in WCAG 3 is yet to be determined,” with requirements merely assuming a size/weight factor. W3C keeps WCAG 2.x contrast normative for conformance today; WCAG 2.2 shipped October 2023 with no contrast-formula change, and WCAG 3 conformance remains years out — Bronze, Silver and Gold awards are still marked TBD in the draft’s own conformance tables.
The practical answer is to lint with both: use WCAG for conformance and APCA for perception. When their verdicts diverge, flag the pair for review rather than hiding one result behind the other. The APCA demonstrator is useful for inspecting how signed Lc responds to a given color pair.
FAQ
These answers separate APCA’s usefulness as an agent signal from its current standards status. The short version is that APCA can improve how a design system evaluates text contrast, but it does not replace current WCAG 2.x obligations, and the right threshold depends on the text role being checked.
Is APCA legally required instead of WCAG 2.x?
No. WCAG 2.x remains the normative basis for current conformance, and WCAG 3’s contrast algorithm is still officially undetermined — the September 2026 draft explicitly says so. Use WCAG 2.x checks where compliance is required, and add APCA as a perception-oriented signal. The WCAG 3 draft describes the evolving work; it does not make APCA a replacement requirement.
Why does my 7:1 dark-mode text fail APCA?
A 7:1 ratio is unsigned: it cannot express whether text is light on dark or dark on light, and the +0.05 offset can make near-black pairs look sufficient numerically. APCA preserves polarity in signed Lc and evaluates the pair with different exponents for each direction. A WCAG pass therefore does not guarantee the pair reaches APCA’s fluent-reading floor for its role.
What Lc should an agent require?
Use role-based floors rather than one threshold for every text style: Lc 60+ for body text, Lc 45+ for spot-reading or placeholder text, and Lc 45+ for large headlines. The sign identifies polarity, while magnitude is checked against the role threshold. Treat these as an agent gate, not a substitute for current WCAG conformance.
The Bottom Line
Agents need polarity-aware Lc thresholds, not one ratio. A single 4.5:1 gate fails in both directions: it passes dark-mode text APCA flags and fails light-mode text APCA accepts. Until WCAG 3 lands, lint with both—WCAG for conformance, APCA for perception—and surface divergences for review.
How This Guide Was Built
This post was produced with a small Python script transcribing the published algorithm constants of [email protected]. The transcription reproduced 8/8 package test vectors bit-exact (for example #888 on #FFF = 63.0565 and #def on #123 = −93.0677, matching the package’s own test file) and was validated 2026-10-08. The work consists of desk research and computation only, with no hands-on screen testing; the measured dataset is quoted verbatim above.
