Creative Engineering
Applied4 min read

Contrast is a unit test

A documented WCAG ratio is a claim, and claims rot. How this site turned its colour contrast, token usage rules and reduced-motion floor into tests that fail the build instead of a checklist that fails quietly.

Learned building The Studio

Written and engineered by

Design systems are full of claims. This grey passes AA on our background. This muted tone is for large text only. Reduced motion is respected everywhere. The claims are usually true on the day they are written, and then they are comments — and a comment asserting a contrast ratio is worth nothing the moment someone nudges a hex value.

This site treats those claims the way WPAudio Engine treats a mastering decision: if it cannot be verified against the artefact, it does not count. Three of its accessibility rules run as unit tests in CI. This article shows all three, because each catches a different way the same promise breaks.

The claim, and where it comes from

WCAG 2.1 requires a contrast ratio of at least 4.5:1 between body text and its background, and 3:1 for large text; success criterion 1.4.11 extends the 3:1 floor to meaningful UI components. The ratio itself is defined arithmetically from the relative luminance of the two colours (both terms are defined in WCAG 2.1’s glossary) — which is the important part for what follows: it is a pure function of two hex values. Anything that is a pure function of the source can be a unit test of the source.

Test one: the token file tells the truth

Our token file documents a measured ratio beside every colour:

--color-secondary: #9ba1ac; /*  7.58:1 — AA body, AA large */
--color-tertiary: #5c6270; /*  3.22:1 — AA large/UI ONLY. Never body copy. */

The first test reads tokens.css with a regex — the real file, not a copy of its values — computes each ratio with the WCAG relative-luminance formula, and asserts it matches the documented figure. Rename a token, delete one, or edit one into non-compliance and the suite fails with the token’s name in the message. The comment cannot drift from the colour, because the comment is asserted against the colour.

Test two: the usage rule is enforced, not aspirational

A verified ratio can still be misused. Our --color-tertiary measures 3.22:1 — legitimately above the 3:1 floor for large text and UI, legitimately below the 4.5:1 floor for body copy. The rule “tertiary never carries small text” lived in a comment. A review then found two dozen places where text-tertiary had been paired with caption-sized text anyway — every one of them failing AA, on a site that unit-tests its contrast table.

A verified value plus an unverified usage rule is a false sense of safety. So a second test scans every component’s class attributes for a small text size and text-tertiary together, and fails with file and line number. When a React island later joined the Astro components, the test grew a className clause — the rule follows the code, whatever the framework spells its attribute.

Test three: the floor that must outrank everyone

Reduced motion is handled globally: a prefers-reduced-motion block neutralises every animation and transition, deliberately marked !important so that motion authored later — by someone who forgot the rule — still cannot reach a visitor who asked their OS not to receive it. That floor is one well-intentioned refactor away from disappearing, so the third test asserts that the media query and its !important declarations still exist in the stylesheet source. Removing the floor fails CI before it fails a person.

Why bother, when audits exist

An audit is a snapshot; these are tripwires. They cost a few milliseconds per test run and they moved three accessibility promises from “believed” to “enforced”. The pattern generalises to anything that is a pure function of your source files: if the design system makes a claim, make the repository prove it — the same principle this Studio applies to audio measurements, applied to its own CSS.

What the tests do not do is also worth stating: they cannot judge whether a screen-reader experience makes sense, whether focus order is humane, or whether alt text says anything useful. Those need people. The tests exist so that the mechanical failures never waste a person’s attention again.

Postscript: the test missed one

In August 2026 a routine Lighthouse run found a contrast failure on this very page — the date and reading-time line, text-tertiary at 13px, 3.21:1. The usage test described above was green throughout, and would have stayed green forever.

The reason is worth more than the fix. The rule matched a size class and a colour class inside one class attribute. But font size is inherited, and the markup put the size on the parent and the colour on the child, so the two facts the rule compared were never in the same string. The rule could not fail for input shaped that way.

The check now walks the rendered ancestor chain instead of reading source, and its regression case is that exact parent/child split — verified by putting the bug back and watching the new check go red while the old one stayed green. The full account is in ADR 0008.

Which makes the title of this article true in a narrower sense than it first claimed: contrast is a unit test, and a unit test is only worth the scope of what it actually asserts.

References

  • W3C, Web Content Accessibility Guidelines (WCAG) 2.1 — contrast minimum (SC 1.4.3), non-text contrast (SC 1.4.11), and the relative luminance definition — w3.org/TR/WCAG21
  • MDN Web Docs, prefers-reduced-motiondeveloper.mozilla.org