Making Contrast a Build Constraint
How this site turns APCA contrast targets into build-time checks for semantic color roles across light and dark themes.
A color palette can look balanced in a design review and still drift as the site changes. A lighter metadata color, a new surface, or a brighter dark-mode accent can make text harder to read without creating an obvious failure in the code.
This site now treats contrast as a build constraint. A validator checks 32 text-and-background roles across light and dark themes, reports combinations that miss their targets, and stops the build when a required floor is crossed.
The goal is not to automate visual judgment. It is to make the decisions behind the palette explicit and harder to regress.
Check Roles, Not Isolated Colors
Contrast belongs to a pair of colors in a specific context. A cyan token does not have a meaningful score by itself. Cyan used for a link on paper does. The same link on surface is another decision.
The validator therefore describes roles such as:
- body prose on the page and on raised surfaces
- metadata and keyboard hints
- links and hover states
- tags, selected chips, and primary buttons
- small gradient labels
- placeholders with transparency applied
Each role identifies a foreground token, a background token, and a minimum Lightness Contrast value:
{
mode: "light",
name: "links: cyan on paper",
text: "cyan",
background: "paper",
minLc: 75,
}
This is more useful than testing every palette combination. The check stays small because it represents combinations the interface actually uses.
Use Contrast Levels That Match the Text
APCA reports perceived lightness contrast as an Lc value. It is polarity-aware, so dark text on a light background and light text on a dark background are evaluated in the correct order. Its guidance also relates contrast to the size and weight of the text instead of treating every text role as equivalent.
The site uses stricter floors for body copy and small text that must be read. Supporting content, large heavy headings, placeholders, and non-text elements can use different levels appropriate to their jobs.
That distinction matters. A color that is comfortable for a large display heading may not be strong enough for a 13-pixel label. One universal threshold would either miss that problem or unnecessarily flatten the rest of the palette.
Make the Build Enforce the Decision
The validator reads the sRGB fallback values from the light and dark theme tokens. It resolves each role, blends transparent text against its background when necessary, and calculates the APCA score with apca-w3.
A result below its required floor fails the production build:
Contrast error: dark: metadata: faint on paper scores Lc -46.3,
below the Lc 60 floor
The score is negative because APCA signs its output: light text on a dark background reports a negative Lc, and the floor is compared against the magnitude. That sign is information rather than noise, since a pair that flips polarity is a different design decision, not a rounding difference.
Some roles also have an advisory target above their current floor. Those combinations pass, but the build reports the remaining gap. This keeps the hard requirements honest while leaving a visible path for later refinement.
The contrast check runs with the same validation command that checks metadata, performance budgets, security policy, links, and the generated search index. A color change is therefore tested as part of the site, not as a separate task someone has to remember.
Let a Failed Check Change the Design
The first useful result was not a passing report. It was a list of colors that needed attention.
Light metadata became darker. Dark metadata became lighter: faint moved from #8f95c3 to #adb2df, which carried the role from Lc -46.3 to Lc -62 and cleared its floor. Link and hover colors shifted enough to meet their roles while remaining recognizably part of the same palette. The small eyebrow gradient changed from the brightest electric colors to the mode-aware cyan, violet, and magenta tokens.
That last change clarified an important boundary. The electric colors still work for glows, the logo, the favicon, and other non-text moments. They do not need to carry 13-pixel text simply because they are visually expressive.
Accessibility did not remove the accent palette. It gave each part of the palette a more appropriate job.
Keep the Limits Visible
This check is deliberately narrower than a browser audit. It evaluates known token pairs using their sRGB fallbacks. It does not inspect every computed style, infer the rendered size and weight of every label, or understand text placed over images and complex layered backgrounds.
It is also a design guardrail, not a declaration of accessibility conformance. Rendered pages still need review in both themes, at different viewport sizes, and through keyboard and assistive-technology workflows.
The useful boundary is straightforward: automate the stable decisions and manually inspect the contextual ones.
A design token becomes more trustworthy when it carries both a visual purpose and a measurable constraint. Putting those constraints in the build keeps contrast from becoming a one-time audit and turns it into part of how the site is maintained.