---
title: "Contrast conformance"
description: "The measured contrast of every published token pair, in both themes, generated and dated rather than asserted."
url: "https://opsinjs.pensievelabs.org/accessibility/contrast-conformance"
source: "https://opsinjs.pensievelabs.org/accessibility/contrast-conformance.md"
section: "Accessibility"
kind: "accessibility"
reviewed: "2026-09-02"
reviewer: "design"
aliases: ["measured contrast", "contrast results", "1.4.3", "1.4.11"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="accessibility" />

## What we guarantee [#what-we-guarantee]

Every colour pair that opsinjs publishes as a pair has been measured, and the
measurement is on this page rather than in a spreadsheet somebody once made.

Three specifics, because "we meet WCAG AA" is a claim with more escape hatches
than it looks:

* **Two numbers, always.** Each pair is reported both as a WCAG 2.2 contrast
  ratio and as an APCA lightness contrast. They answer different questions and
  they disagree in predictable places. The disagreement is most visible on
  light text over a mid-tone surface, where the WCAG ratio is optimistic. The
  conformance answer is the ratio; the *is this actually readable* answer is
  closer to the APCA figure. Why we publish both is on
  [Contrast and APCA](../foundations/colour/contrast-and-apca.mdx).
* **Both themes.** A pair that passes in light and fails in dark is a failing
  pair. The dark theme is not a filter over the light one; it has its own
  authored values in `app/globals.css` and `app/product.css` and its own
  measurements.
* **Failures are shown, not filtered.** A pair that does not clear the published
  floor appears on this page marked as failing, with the reason it is still
  shipped if it is. Removing failures from a report is how a report becomes
  worthless.

The floor itself, and the argument for it, is on
[The contrast floor](../foundations/materials/the-contrast-floor.mdx).

## What you own [#what-you-own]

**Any pair you invent.** Put text on a colour that is not a published pair and
the measurement on this page does not cover it. A brand colour, a photograph, a
chart series and a gradient are all colours of that kind, and pairing text with
one of them is the most common way a product built on a well-measured design
system ends up with unreadable text.

**Text over translucent material.** Rungs 3 and 4 of the material ladder are
translucent and blurred, so the effective background depends on what is behind
them. The system publishes the measurement against the ladder's declared opaque
fallback; the *worst case* over your particular content is yours to check. See
[The contrast floor](../foundations/materials/the-contrast-floor.mdx) for the
scrim rule.

**Themes you generate.** If you derive a theme from your own brand colour, the
published numbers describe the shipped presets and not your theme. Run the
validator: [Validating your theme](../theming/validating-your-theme.mdx).

**Non-text contrast.** SC 1.4.11 covers the boundary of a control and the
components of a graphic that convey information. The line of a range bar, the
tick on a dial and the border of a focus ring are measured here where they are
token pairs, but the composition you build them into is not.

## How to check [#how-to-check]

<Steps>
  ### Regenerate the numbers [#regenerate-the-numbers]

  Run `pnpm run contrast` in the repository. It executes `scripts/check-contrast.mts`,
  which reads `tokens/*.json`, computes both metrics with the hand-written maths in
  `lib/color/` (no external colour library is installed), writes
  `lib/generated/contrast.json` and rewrites the generated reference page. The build
  then asserts the result is unchanged, so a token edit that moves a measurement
  fails CI rather than shipping quietly.

  ### Check one pair by hand [#check-one-pair-by-hand]

  Use the pair checker at [`/playground/contrast`](/playground/contrast), or inline:

  <ContrastOracle />

  Paste the two resolved colour values rather than the token names if you want to
  verify the generator rather than trust it.

  ### Check a pair the system does not publish [#check-a-pair-the-system-does-not-publish]

  Read the two effective colours out of the browser and measure those. Take them
  from the computed style rather than from the authored value, because the gamut
  escalation in `@supports (color-gamut: p3)` changes chroma on a wide-gamut
  display. Lightness is deliberately unchanged by that escalation, so a correct
  measurement on an sRGB display remains correct on a P3 one.
</Steps>

## Measured results [#measured-results]

<ContrastReport scope="all" />

<NoDataYet script="scripts/check-contrast.mts" />

## Known gaps [#known-gaps]

* **No component pairs are measured,** because no component exists to declare
  which pairs it uses. Today this page covers the token layer only.
* **Translucent rungs are measured against their opaque fallback,** not against a
  distribution of real backdrops. That is a deliberate simplification and it is
  optimistic; the scrim rule exists to cover it.
* **Chart colours are not measured against each other.** Adjacent-series
  discriminability is a different property from text contrast and is not yet
  computed. See
  [Chart colour](../foundations/data-visualisation/chart-colour.mdx).
* **APCA is a draft.** It is not a conformance standard and is published here as
  the better readability predictor, not as a claim of conformance to anything.

## Updates to this page [#updates-to-this-page]

<Reviewed />
