The thesis: a heuristic is agent-checkable when it’s a function of readable inputs
Text contrast is a design criterion an AI agent can genuinely learn to check, but only when the rule is a closed-form function of machine-readable inputs — an argument this essay makes by running WCAG 2.2 SC 1.4.3 (per the W3C WAI Understanding page for SC 1.4.3) and APCA 0.0.98G-4g (per the APCA documentation at git.apcacontrast.com, fetched 2026-11-10) over the same eight colour pairs and finding three disagreements. This is the criteria half of “how can AI agents learn to design better,” framed through the perception half: a rule is agent-checkable only when every input is a value the agent can read from computed CSS. We used the same framing in our design-system AI-readability check, which asks the same question about inputs before it judges any criterion.
A computable rule is a function from readable values to a verdict the agent can defend. Contrast is the cleanest test case because its inputs are ordinary CSS values and its exemptions are few. The two rules below are both computable; the third thing people ask for is not.
What the agent perceives: five values from computed CSS
An agent can perceive exactly five contrast-relevant values from computed CSS — foreground colour, resolved background colour, font size, font weight, and the polarity between the pair — and the W3C Understanding page for SC 1.4.3 explicitly says to evaluate specified foreground/background colours rather than anti-aliased pixels, which makes contrast unusually agent-friendly (MDN’s WCAG colour-contrast summary states the same 4.5:1 and 3:1 thresholds for readers coming from the developer side). In our own pipeline the background is resolved by walking ancestors and compositing rgba layers until opaque; where the base is an image, gradient, video or canvas, we return UNRESOLVED rather than guess. Font size is read in px (1pt = 1.333px), font weight of 700 or above counts as bold, and polarity is DARK_ON_LIGHT if Y(fg) < Y(bg), otherwise LIGHT_ON_DARK. That last W3C sentence matters: the standard tells you to use the numbers, not the rendered bitmap, which is the same reason an agent contract should name its inputs explicitly.
Rule 1 — WCAG 2.2 SC 1.4.3 as a pure function
WCAG 2.2 SC 1.4.3 reduces to a pure function — L = 0.2126R + 0.7152G + 0.0722B with channels c <= 0.04045 ? c/12.92 : ((c+0.055)/1.055)^2.4, ratio = (L1+0.05)/(L2+0.05), pass at >= 4.5:1 (>= 3:1 large-scale, i.e. 18pt or 14pt bold — the page states 1pt = 1.333px, giving ~24px and 18.66px, which it rounds to 24px and 18.5px) — and the page is explicit that “4.499:1 would not meet the 4.5:1 threshold”. The function is symmetric in its two luminances, so foreground and background can be treated as an unordered set for rule 1 only.
Where the thresholds come from
The 4.5:1 value is not arbitrary: 3:1 comes from ANSI/HFS 100-1988 and ISO-9241-3, multiplied by about 1.5 contrast-sensitivity loss at about 20/40 acuity; 7:1 is SC 1.4.6 (AAA) for about 20/80. These are the rationales stated in the W3C material; they are not claims about any individual reader. See also the contrast-enhanced understanding page.
Operational scope
SC 1.4.3 applies to placeholder, hover, and focus text, and is evaluated from specified foreground/background colours. Exemptions cover incidental text, pure decoration, and logotypes. A logo acting as a UI control remains exempt, but W3C encourages a compliant variant; a logo darkened as an author choice is explicitly not exempt.
Rule 2 — APCA 0.0.98G-4g as a signed function
APCA 0.0.98G-4g (per the APCA docs at git.apcacontrast.com, fetched 2026-11-10) is also a pure function but a signed one: Y = (R/255)^2.4·0.2126729 + (G/255)^2.4·0.7151522 + (B/255)^2.4·0.0721750 with a soft clamp below 0.022, polarity-dependent exponents (0.56/0.57 dark-on-light; 0.65/0.62 light-on-dark), ×1.14, ±0.027, ×100 — and the sign of the returned Lc encodes polarity (positive = dark text on light background). The range is roughly Lc 0 to ±Lc 106, and doubling Lc is meant to feel like doubling perceived contrast.
The provisional floors
The APCA Easy Intro provisional levels are: Lc 90 preferred for columns of body text at 14px/400+; Lc 75 minimum for body text at 18px/400; Lc 60 minimum for content text that is not body; Lc 45 minimum for larger text; Lc 30 absolute minimum for any other text; Lc 15 absolute minimum for non-text that must be discernible, where “Lc 15 is the point of invisibility for many users.” The dark-mode maximum is Lc -90 for large fonts, marked preliminary. These are provisional, not final.
The optional lookup table
Size and weight handling is carried by an optional lookup table; the general levels work without it. This post does not apply that table, and the floor column in the table below is this blog’s own editorial mapping of the provisional levels onto each pair’s role.
APCA’s stated case against WCAG 2.x
APCA’s documentation argues that “WCAG 2.x … far overstates contrast for dark colors” and that “4.5:1 can be functionally unreadable when a color is near black.” That is APCA’s position, attributed to the APCA documentation, never W3C’s. The W3C TR WCAG 2.2 does not say this.
The comparison: eight pairs, two verdicts, three disagreements
Running both rules over eight pairs from this blog’s global.css yields three disagreements out of eight, in both directions, per measurements from this blog’s self-tested script (validated against all eight APCA README keystone values to within 0.01).
| Role | Pair | WCAG ratio | WCAG | APCA Lc | Floor | APCA |
|---|---|---|---|---|---|---|
| Body text | #e8e8ed on #0a0a0b | 16.21 | PASS | −93.0 | 75 | PASS |
| Secondary prose | #a1a1aa on #0a0a0b | 7.72 | PASS | −51.6 | 60 | FAIL |
| Muted meta | #6b6b76 on #0a0a0b | 3.76 | FAIL | −25.4 | 60 | FAIL |
| Muted on card | #6b6b76 on #121214 | 3.55 | FAIL | −25.0 | 60 | FAIL |
| Accent link | #b8422e on #0a0a0b | 3.63 | FAIL | −25.3 | 60 | FAIL |
| Lighter accent | #d46a55 on #0a0a0b | 5.65 | PASS | −39.4 | 60 | FAIL |
| Label on accent btn | #e8e8ed on #b8422e | 4.46 | FAIL | −66.9 | 60 | PASS |
| Prose on raised surf | #e8e8ed on #1a1a1e | 14.21 | PASS | −91.8 | 60 | PASS |
The floor column is this blog’s own editorial mapping of the Easy Intro provisional levels onto each pair’s role (body at 18px/400 → 75; secondary/content → 60). It is non-normative and no lookup table was applied. Two dark-mode pairs pass WCAG but fail APCA (7.72 versus −51.6; 5.65 versus −39.4), consistent with APCA’s dark-colour argument. One pair fails WCAG but passes APCA (4.46 versus −66.9, light text on the mid-red accent), decided by the no-rounding rule. Three of eight pairs disagree in total.
Polarity: the input WCAG throws away
Swapping foreground and background changes nothing in WCAG (3.63:1 both directions for the accent pair, 3.76 both ways for muted) but flips APCA’s Lc in both sign and magnitude (−25.3 versus +27.6, a 53.0 Lc swing, for the accent pair; −25.4 versus +27.7, 53.1, for muted), because APCA’s exponents differ by polarity, which means an agent that cannot resolve the true background cannot run APCA at all. The ratio is symmetric by construction, so treating the pair as an unordered set is safe for rule 1 and invalid for rule 2. Floor comparison must be sign-aware: dark-on-light passes when Lc >= +floor; light-on-dark passes when Lc <= -floor.
What breaks when the agent asks for “WCAG 3 contrast”
“WCAG 3 contrast” cannot be checked by any agent today, because the WCAG 3.0 Working Draft of 10 September 2026 gives the outcome “Text contrast sufficient (minimum)” the literal measure “@@[contrast measure to be determined]” — the draft’s own @@ marker for a value it has not filled in yet — and notes “The contrast algorithm used in WCAG 3 is yet to be determined”, with the string “APCA” appearing zero times in the 361,798-character draft fetch. Both the minimum and enhanced outcomes carry that note, which assumes the future algorithm “will include a size/weight factor”; “contrast ratio test” is Exploratory and “color contrast” is Developing. Any tool emitting a WCAG 3 contrast verdict is asserting something the standard does not contain.
The computable rules: predicates an agent can run
The deliverable of this essay is a set of runnable predicates — PERCEIVE, WCAG_2_2_1_4_3, APCA_LC, APCA_FLOOR, a WCAG_3 refusal, and one emission schema — every constant taken from the W3C Understanding page and the APCA 0.0.98G-4g documentation.
PERCEIVE(node) -> {fg, bg, size_px, weight, polarity} | UNRESOLVED(reason)
fg = computed color, alpha composited over bg
bg = walk ancestors compositing rgba layers until opaque
image/gradient/video/canvas base -> UNRESOLVED (never guess)
size_px = computed font-size (pt -> px at 1pt = 1.333px)
weight = computed font-weight (>= 700 counts as bold)
polarity = DARK_ON_LIGHT if Y(fg) < Y(bg) else LIGHT_ON_DARK
# use specified fg/bg colours, never anti-aliased pixels
WCAG_2_2_1_4_3(p) -> Verdict
c = ch/255 ; lin = c <= 0.04045 ? c/12.92 : ((c+0.055)/1.055)^2.4
L = 0.2126R + 0.7152G + 0.0722B
ratio = (max(Lfg,Lbg)+0.05) / (min(Lfg,Lbg)+0.05)
large = size >= 24 OR (size >= 18.66 AND weight >= 700)
pass = ratio >= (4.5 if not large else 3.0) # raw compare; 4.499 FAILS
APCA_LC(p) -> Verdict
Y = (R/255)^2.4*0.2126729 + (G/255)^2.4*0.7151522 + (B/255)^2.4*0.0721750
Ys = Y < 0.022 ? Y + (0.022 - Y)^1.414 : Y
(eBG,eTXT) = (0.56,0.57) if DARK_ON_LIGHT else (0.65,0.62)
v = (Ys(bg)^eBG - Ys(fg)^eTXT) * 1.14
Lc = (v > 0 ? v - 0.027 : v + 0.027) * 100 # sign = polarity; ~0..+-106
APCA_FLOOR(role) # Easy Intro, PROVISIONAL
body columns 14px/400+ -> 90 ; body 18px/400 -> 75 ; content non-body -> 60
larger text -> 45 ; any other text -> 30 ; non-text discernible -> 15
pass = Lc >= +floor (dark-on-light) OR Lc <= -floor (light-on-dark)
emit caveat: editorial role mapping, no lookup table applied
dark-mode cap: Lc -90 for large fonts (preliminary)
WCAG_3(p) -> NOT_COMPUTABLE
reason: outcome specifies "@@[contrast measure to be determined]" (WD 2026-09-10)
0 occurrences of "APCA" in the draft fetch
fallback: report WCAG 2.2 SC 1.4.3 and/or APCA 0.0.98G-4g, each labelled
EMIT {selector, method, standard_version, fg, bg, size_px, weight, polarity,
value, threshold, verdict, caveats[], provenance{rule_version,
script: "validated against 8 APCA README keystone values, max |Δ| 0.01"}}
REFUSES: no numeric verdict attributed to WCAG 3; no conformance verdict for
exempt text (incidental/decoration/logotype); no pass after rounding;
no verdict without method+version; UNRESOLVED pairs stay UNRESOLVED;
no claim that ratio or Lc predicts readability for any individual.
This predicate set is the kind of computable contract an agent can run without asking a follow-up question.
How to verify: reproduce the check, not the opinion
Verification of a contrast checker is itself computable: implement the algorithm, validate against the eight keystone values published in the APCA README (this blog’s Python script matches all eight to within 0.01), then re-run the eight pairs and assert the published table — no user study is required or claimed. Pin versions: APCA docs fetched 2026-11-10; WCAG 3.0 Working Draft 10 September 2026. Licence boundary: the npm badge reads “Beta Non-Com”; the README calls the basic code “a public beta for non-commercial use and testing only”; medical, clinical, aerospace, transportation, and military uses are prohibited without a written licence; and “WCAG 3 is not an official standard yet.” See the SAPC-APCA repository for implementation context.
Limitations: where the predicates overstate
These predicates overstate certainty in specific, nameable ways: APCA’s floors are provisional, this blog’s role-to-floor mapping is editorial and applies no lookup table, SC 1.4.3’s exemptions (incidental text, decoration, logotypes) need human judgment, and the APCA licence is non-commercial with prohibited domains — all per the APCA documentation and the W3C Understanding page. Keystone validation proves arithmetic fidelity, not threshold validity. Choosing between two disagreeing rules is an editorial act. In our pipeline placeholder, hover and focus states are located in the DOM before any ratio is computed, which is a perception problem the formula does not solve. Anti-aliasing is ignored by design, as the W3C page directs. A check like this is still useful when catching design token drift, but it cannot settle which rule your product should prefer.
FAQ
Can my agent check “WCAG 3 contrast” today?
No. The WCAG 3.0 Working Draft of 10 September 2026 leaves the outcome measure as “@@[contrast measure to be determined]” and says the contrast algorithm is yet to be determined. Emit a refusal and report WCAG 2.2 SC 1.4.3 or APCA 0.0.98G-4g, each with its method and version.
Why add APCA to a linter that already reports ratios?
APCA consumes polarity and provisional, size- or role-aware floors. On this palette it flags dark-mode secondary text (7.72 pass versus −51.6 versus a floor of 60) and lighter accent text (5.65 pass versus −39.4 versus 60), while the accent-button label fails WCAG but passes APCA at −66.9.
Why do the two rules disagree on the same pair?
They are different functions. WCAG 2.2 uses a symmetric ratio; APCA uses signed, polarity-dependent lightness contrast. Three of eight pairs here disagree, and the disagreement runs both ways, including 4.46 failing WCAG while −66.9 passes APCA. The agent should emit both verdicts, labelled.
Does the Lc sign matter in my pipeline?
Yes. The sign encodes polarity, and the exponents differ by polarity, so comparing absolute Lc or swapping foreground and background without recomputing gives wrong verdicts. The accent pair reads −25.3 one way and +27.6 the other, a 53.0 Lc swing, despite an unchanged 3.63:1 ratio.
Can I ship APCA in a commercial product?
Not as-is. The npm badge reads “Beta Non-Com”, the README calls the basic code a public beta for non-commercial use and testing only, and medical, clinical, aerospace, transportation, and military uses are prohibited without a written licence. Treat it as an experimental signal until licensed.
The Bottom Line
WCAG 2.2 SC 1.4.3 is a rule an agent can run and defend today. APCA 0.0.98G-4g is runnable and more informative, but carries provisional thresholds and a non-commercial licence. “WCAG 3 contrast” is a placeholder an honest agent must refuse to score. The meta-lesson for agent design is that a criterion becomes agent-learnable when it is a function of perceivable inputs; when two functions disagree, emit both, labelled; never emit a verdict for a standard that has not chosen its math. The same discipline applies to z-index stacking contexts: name the inputs, name the version, refuse the undefined.
How This Guide Was Built
This guide was built from documentation (the W3C WAI Understanding page, the APCA docs at git.apcacontrast.com fetched 2026-11-10, the WCAG 3.0 Working Draft of 10 September 2026), published constants, and a self-tested Python implementation validated against the APCA README’s eight keystone values — not from hands-on user testing. The W3C standards types page is useful context for why a Working Draft is not a recommendation.
