contrastFactor — that multiplies every APCA target before the solver runs. The brand’s hues and chromas are untouched; only the lightness the solver lands on changes. APCA attribution and use restrictions: see About APCA.
That distinction matters in practice: a high-contrast Substrate interface still looks like your product, because the only thing that moved is how far apart the foreground and background sit on the L axis.
The policy and the multiplier
The engine’s APCA policy is fixed and named. Three targets, all measured in APCA Lc against the context surface:contrastFactor scales all three together. It runs from 0.75 to 1.5, defaults to 1, and is bounded per brand by the flexibility.contrast range in the brand config — a brand can narrow how far its users may push the axis.
The two high-contrast presets both set it to 1.3:
Contrast is a root-level preference scalar, not a per-subtree attribute. You do not mark a container as high-contrast. What is scoped is the surface a solve runs against: a nested surface re-solves its foregrounds against its own background, at whatever
contrastFactor is currently set.Setting it
Like every axis, you set the number and re-sync:1.15 is a real, solvable preference — presets are just the labelled stops.
Following the operating system
The runtime ships a helper that maps the OS contrast preference onto the axis and keeps listening for changes. It handles both directions:prefers-contrast: more→contrastFactorbecomes 1.3prefers-contrast: less→contrastFactorbecomes 0.85
less direction is easy to overlook. Users who find high contrast uncomfortable — a common need for some forms of visual sensitivity and dyslexia — are served by the same axis running the other way, which is precisely why contrast is continuous rather than a boolean.
A related but separate signal is forced-colors: active (Windows High Contrast). There the OS supplies the palette outright, so the runtime disables CVD compensation and lets the system colors through rather than competing with them.
What the no-JS floor covers
Two of the four baked snapshots are the high-contrast ones, gated onprefers-contrast: more:
contrastFactor — including prefers-contrast: less at 0.85, and any user-chosen intermediate value — needs the runtime, as do nested surfaces. The snapshot is the accessible floor, not the whole model.
When a target can’t be met
The solver does not silently pass. It binary-searches the OKLCH lightness that hits the scaled target, and when the gamut runs out before the target is reached it records the shortfall as anunmetLc value on the solution rather than pretending it succeeded.
At generation time this is enforced: the pipeline runs an accessibility gate across the whole brand corpus and fails the build on shortfalls, with output naming the count and kind of failure. Pushing contrastFactor toward 1.5 on a brand with very high chroma is the usual way to hit this — the fix belongs in the brand config, either by widening flexibility.contrast bounds deliberately or by adjusting the intent’s chroma. See the APCA solver reference.
Light & Dark
The scheme axis that composes with contrast, and what the runtime writes.
Color Vision
CVD compensation, which runs before the contrast solve rather than after it.