opsinjs
FoundationsToken familiesColour

Contrast and APCA

Both contrast models side by side. WCAG 2.2 is the conformance obligation, APCA the design tool, and this page states the floor the system will not ship below.

Overview

There are two ways to put a number on how readable text is against its background, they disagree, and most design systems quietly pick one and hope nobody notices. opsinjs publishes both, for every measured pair, in both themes, and states which one it is bound by:

WCAG 2.2 is the obligation. APCA is the tool.

A pair ships only if it clears the WCAG 2.2 requirement, because that is what conformance, procurement and the law are written against. APCA is what the derivation engine optimises with, and it is published so that a designer can see the cases where a pair is technically conformant and still hard to read.

The thing this is most often confused with is a contrast checker. A checker tells you about one pair you thought to check. This page is about a floor that is applied to the whole declared pair set, measured in CI, with the gated set held against regression by a nightly run, so that nobody has to remember to check.

How it works

WCAG 2.2 contrast ratio

The ratio is computed from relative luminance:

ratio = (L_lighter + 0.05) / (L_darker + 0.05)

where L is relative luminance from the sRGB channel values after linearisation. It runs from 1:1 to 21:1. Its properties, stated without enthusiasm:

  • It is symmetric. Black text on white and white text on black score identically, although they do not read identically, particularly at small sizes where light-on-dark halates.
  • It is size-blind except for one carve-out, so 14px and 40px text are held to the same figure. The carve-out is the 3:1 allowance for large text.
  • The + 0.05 constant compresses the dark end, which is why two dark greys that are visibly different can score almost identically, and why dark themes are the ones that most often look fine and measure badly.

It is nonetheless the number that matters, because SC 1.4.3, SC 1.4.11 and every conformance claim built on them are written in terms of it.

APCA lightness contrast (Lc)

APCA is the Accessible Perceptual Contrast Algorithm, developed as part of the work toward WCAG 3, and it produces a value called Lc on a roughly 0 to 106 scale. It differs from the ratio in three ways that matter:

  • It is polarity-signed. Dark text on a light background gives a positive Lc; light text on a dark background gives a negative one. The two are not interchangeable, and the model says so.
  • It is size- and weight-aware. The published guidance is a lookup table: the Lc you need depends on the font size and weight you are actually using, so a 40px result value and a 13px unit label are not asked for the same number.
  • It behaves sensibly at the ends. Two dark greys that WCAG 2.2 cannot tell apart have visibly different Lc.

The important caveat, and it is the reason APCA is not our gate: APCA is a draft. It is not a normative part of WCAG 2.2, its lookup tables have changed across revisions, and a conformance claim cannot be built on it today. We therefore pin our own Lc thresholds in tokens/color.json, record the date we pinned them, and re-examine when the upstream draft moves. We do not import a moving target into a health product's safety argument.

The published floor

These are thresholds we adopt, not measurements. They are what scripts/check-contrast.mts tests every declared pair against, in light and dark. Where the two models disagree, both have to pass.

What the pair carriesWCAG 2.2APCA |Lc|
A health value a person may act on≥ 4.5:1≥ 90
Body text, labels, ordinary UI copy≥ 4.5:1≥ 75
Large text at 24px+ regular or 19px+ bold≥ 3:1≥ 60
A meaningful line: status rule, chart stroke, dial ring, focus ring≥ 3:1≥ 45
Disabled or purely decorative, carrying no informationnonenone

Two of those rows are opinions rather than requirements and are marked as such:

The floor above is not a single switch. Every measured pair sits in one of three states, and whether a pair that falls below its floor blocks a build depends on which. The tables further down print each pair as Pass or Below floor without naming its state, so the three are defined here.

Gated. The pair is in the regression baseline. A commit that drives it below its floor fails the nightly regression run. Body text, an ink role against its own surface or against the page, and the neutral hairlines are gated, because a reader has to read them or leans on them to tell two regions apart.

Advisory. The pair is measured and published, but it sits outside the regression baseline, because it carries recognition rather than words. An accent fill is the case: it may never carry text and may never be an element's only boundary, so the nightly regression run does not block on it. A non-text advisory pair that measures below its floor does fail the --strict run, and its count is published beside the "failing" figure, so a "0 failing" total can never stand on its own while an accent or a hairline sits below the non-text floor.

Exempt. The pair is outside the floor's scope, and the reason sits beside it. A band fill measured against the page, a status tint measured on a card, a status ring measured against its own surface, and a placeholder fill on the page are exempt: none of them is text, and none is the only thing separating two regions, so no floor applies.

Where the numbers on this site come from

Every figure published on this site is measured, in CI, by hand-written implementations of both models in lib/color/, and rendered by <ContrastReport>. None is typed by a person, and a regression is caught by the nightly run rather than by an individual pull request. The current results for the two clinical axes:

PairThemeAPCA LcWCAG 2.2Floor
attention ink on attention surfacelight89.410.35:1Pass
attention ink on attention surfacedark-81.112.54:1Pass
attention line on attention surfacelight70.04.90:1Pass
attention line on attention surfacedark-65.59.54:1Pass
attention accent on attention surfacelight65.14.18:1Pass
attention accent on attention surfacedark-49.46.80:1Pass
attention ink on the attention bandlight22.42.48:1Below floor
attention ink on the attention banddark-31.01.84:1Below floor
attention accent on the pagelight67.84.35:1Pass
attention accent on the pagedark-50.57.47:1Pass
attention ink on the pagelight92.210.77:1Pass
attention ink on the pagedark-82.313.76:1Pass
steady ink on steady surfacelight88.39.62:1Pass
steady ink on steady surfacedark-82.912.56:1Pass
steady line on steady surfacelight67.54.45:1Pass
steady line on steady surfacedark-68.99.91:1Pass
steady accent on steady surfacelight56.43.21:1Pass
steady accent on steady surfacedark-53.27.25:1Pass
steady ink on the steady bandlight30.03.00:1Below floor
steady ink on the steady banddark-28.81.73:1Below floor
steady accent on the pagelight58.63.31:1Pass
steady accent on the pagedark-54.58.19:1Pass
steady ink on the pagelight90.69.94:1Pass
steady ink on the pagedark-84.214.20:1Pass
unknown ink on unknown surfacelight89.59.98:1Pass
unknown ink on unknown surfacedark-82.212.56:1Pass
unknown line on unknown surfacelight69.24.64:1Pass
unknown line on unknown surfacedark-67.29.72:1Pass
unknown accent on unknown surfacelight43.12.29:1Below floor
unknown accent on unknown surfacedark-51.37.05:1Pass
unknown accent on the pagelight45.62.37:1Below floor
unknown accent on the pagedark-52.37.84:1Pass
unknown ink on the pagelight92.010.34:1Pass
unknown ink on the pagedark-83.113.97:1Pass
urgent ink on urgent surfacelight88.210.58:1Pass
urgent ink on urgent surfacedark-80.312.52:1Pass
urgent line on urgent surfacelight70.05.09:1Pass
urgent line on urgent surfacedark-64.89.52:1Pass
urgent accent on urgent surfacelight72.35.58:1Pass
urgent accent on urgent surfacedark-48.06.63:1Pass
urgent ink on the urgent bandlight13.91.90:1Below floor
urgent ink on the urgent banddark-31.51.89:1Below floor
urgent accent on the pagelight75.35.84:1Pass
urgent accent on the pagedark-49.57.24:1Pass
urgent ink on the pagelight91.211.06:1Pass
urgent ink on the pagedark-81.813.66:1Pass
watch ink on watch surfacelight89.410.02:1Pass
watch ink on watch surfacedark-82.112.58:1Pass
watch line on watch surfacelight69.54.72:1Pass
watch line on watch surfacedark-66.89.68:1Pass
watch accent on watch surfacelight61.73.72:1Pass
watch accent on watch surfacedark-50.76.95:1Pass
watch ink on the watch bandlight25.72.70:1Below floor
watch ink on the watch banddark-30.61.81:1Below floor
watch accent on the pagelight64.23.85:1Pass
watch accent on the pagedark-51.77.71:1Pass
watch ink on the pagelight91.910.38:1Pass
watch ink on the pagedark-83.113.95:1Pass
attention surface on the card materiallight0.01.09:1Below floor
attention surface on the card materialdark0.01.00:1Below floor
steady surface on the card materiallight0.01.08:1Below floor
steady surface on the card materialdark0.01.03:1Below floor
urgent surface on the card materiallight0.01.09:1Below floor
urgent surface on the card materialdark0.01.01:1Below floor
watch surface on the card materiallight0.01.08:1Below floor
watch surface on the card materialdark0.01.01:1Below floor
the focus ring on the attention surfacelight67.34.44:1Pass
the focus ring on the attention surfacedark-45.96.30:1Pass
attention ink on the neutral hover filllight88.610.23:1Pass
attention ink on the neutral hover filldark-79.310.53:1Pass
the focus ring on the steady surfacelight67.84.47:1Pass
the focus ring on the steady surfacedark-45.86.12:1Pass
steady ink on the neutral hover filllight87.09.43:1Pass
steady ink on the neutral hover filldark-81.310.86:1Pass
the focus ring on the urgent surfacelight67.04.42:1Pass
the focus ring on the urgent surfacedark-45.56.34:1Pass
urgent ink on the neutral hover filllight87.710.50:1Pass
urgent ink on the neutral hover filldark-78.910.46:1Pass
the focus ring on the watch surfacelight67.64.46:1Pass
the focus ring on the watch surfacedark-46.06.24:1Pass
watch ink on the neutral hover filllight88.39.86:1Pass
watch ink on the neutral hover filldark-80.110.67:1Pass
PairThemeAPCA LcWCAG 2.2Floor
activity ink on activity surfacelight88.59.61:1Pass
activity ink on activity surfacedark-83.112.58:1Pass
activity line on activity surfacelight67.64.42:1Pass
activity line on activity surfacedark-69.09.93:1Pass
activity accent on activity surfacelight44.12.34:1Below floor
activity accent on activity surfacedark-53.37.26:1Pass
activity accent on the pagelight46.12.41:1Below floor
activity accent on the pagedark-54.68.22:1Pass
activity ink on the pagelight90.59.90:1Pass
activity ink on the pagedark-84.414.24:1Pass
activity line on the bandlight66.14.32:1Pass
activity line on the banddark-67.48.60:1Pass
heart ink on heart surfacelight89.110.51:1Pass
heart ink on heart surfacedark-80.512.55:1Pass
heart line on heart surfacelight70.45.05:1Pass
heart line on heart surfacedark-64.59.46:1Pass
heart accent on heart surfacelight60.33.64:1Pass
heart accent on heart surfacedark-48.26.68:1Pass
heart accent on the pagelight63.23.81:1Pass
heart accent on the pagedark-49.57.27:1Pass
heart ink on the pagelight92.010.97:1Pass
heart ink on the pagedark-81.813.65:1Pass
heart line on the bandlight69.95.01:1Pass
heart line on the banddark-62.87.88:1Pass
labs ink on labs surfacelight88.89.78:1Pass
labs ink on labs surfacedark-83.112.69:1Pass
labs line on labs surfacelight67.94.51:1Pass
labs line on labs surfacedark-68.49.90:1Pass
labs accent on labs surfacelight59.33.49:1Pass
labs accent on labs surfacedark-52.77.21:1Pass
labs accent on the pagelight61.53.60:1Pass
labs accent on the pagedark-53.98.07:1Pass
labs ink on the pagelight90.910.09:1Pass
labs ink on the pagedark-84.314.22:1Pass
labs line on the bandlight66.54.42:1Pass
labs line on the banddark-66.78.49:1Pass
mind ink on mind surfacelight89.910.36:1Pass
mind ink on mind surfacedark-81.312.56:1Pass
mind line on mind surfacelight70.74.94:1Pass
mind line on mind surfacedark-65.49.54:1Pass
mind accent on mind surfacelight60.03.55:1Pass
mind accent on mind surfacedark-49.16.77:1Pass
mind accent on the pagelight62.83.70:1Pass
mind accent on the pagedark-50.07.42:1Pass
mind ink on the pagelight92.710.80:1Pass
mind ink on the pagedark-82.213.77:1Pass
mind line on the bandlight70.04.89:1Pass
mind line on the banddark-63.48.00:1Pass
nutrition ink on nutrition surfacelight89.310.06:1Pass
nutrition ink on nutrition surfacedark-81.912.55:1Pass
nutrition line on nutrition surfacelight69.74.78:1Pass
nutrition line on nutrition surfacedark-66.79.66:1Pass
nutrition accent on nutrition surfacelight39.22.11:1Below floor
nutrition accent on nutrition surfacedark-50.26.88:1Pass
nutrition accent on the pagelight41.82.19:1Below floor
nutrition accent on the pagedark-51.37.63:1Pass
nutrition ink on the pagelight91.910.44:1Pass
nutrition ink on the pagedark-82.913.91:1Pass
nutrition line on the bandlight68.84.71:1Pass
nutrition line on the banddark-64.88.19:1Pass
sleep ink on sleep surfacelight89.610.19:1Pass
sleep ink on sleep surfacedark-81.912.60:1Pass
sleep line on sleep surfacelight70.04.80:1Pass
sleep line on sleep surfacedark-66.49.65:1Pass
sleep accent on sleep surfacelight64.64.05:1Pass
sleep accent on sleep surfacedark-50.46.94:1Pass
sleep accent on the pagelight67.34.22:1Pass
sleep accent on the pagedark-51.47.67:1Pass
sleep ink on the pagelight92.310.60:1Pass
sleep ink on the pagedark-82.913.92:1Pass
sleep line on the bandlight69.14.75:1Pass
sleep line on the banddark-64.58.16:1Pass
activity ink on the card materiallight93.610.34:1Pass
activity ink on the card materialdark-83.512.91:1Pass
heart ink on the card materiallight95.111.47:1Pass
heart ink on the card materialdark-80.912.38:1Pass
labs ink on the card materiallight94.010.54:1Pass
labs ink on the card materialdark-83.512.90:1Pass
mind ink on the card materiallight95.711.28:1Pass
mind ink on the card materialdark-81.412.49:1Pass
nutrition ink on the card materiallight95.010.91:1Pass
nutrition ink on the card materialdark-82.012.61:1Pass
sleep ink on the card materiallight95.411.08:1Pass
sleep ink on the card materialdark-82.112.62:1Pass

Using it

Check a pair now

Your reading is 148/92

Higher than the range your clinic set for you.

WCAG 2.2 ratio
10.86:1
Clears 1.4.3 for body text.
APCA Lc
Perceptual lightness contrast. Polarity-sensitive: swapping the two colours changes it.

The oracle takes any two colours, prints both models, and states the verdict against the floor above. It is the same code path the build uses. If it and the build disagree, that is a bug in this repository, not a rounding difference.

How to read a disagreement

The two models disagree most often in three situations, and each has a different correct response.

Passes WCAG, low Lc. Usually a dark-on-dark pair in a dark theme, where the + 0.05 constant is flattering it. Trust Lc; the pair is genuinely hard to read. This is the single most common way a dark theme ships worse than its light counterpart.

Fails WCAG, comfortable Lc. Usually large, bold, dark-on-light type. It may well be perfectly readable, and you still cannot ship it, because the obligation is the ratio. Adjust the colour.

Both pass, still looks wrong. Check what is actually behind the text. If the answer is a translucent surface, an image or a video, neither number applies, because both models assume a known, opaque background. That case is The contrast floor.

Rules of thumb that are actually true

  • Contrast is a property of a pair, so never say "this colour passes".
  • Fixing contrast by lightening the background changes the design; fixing it by darkening the foreground usually does not. The engine only ever moves the foreground for the same reason.
  • Opacity on text is a contrast change. text-foreground/70 is a new, unmeasured colour, and it is the most common accidental floor violation in Tailwind codebases.
  • Placeholder text is text. So is a disabled label somebody still has to read to understand why the control is disabled.

Tokens

TokenWhat it controlsUsed by
--opsin-neutral-0oklch(1 0 0)Neutral ramp, step 0.no component
--opsin-neutral-50oklch(0.985 0.002 250)Neutral ramp, step 50.no component
--opsin-neutral-100oklch(0.967 0.003 250)Neutral ramp, step 100.no component
--opsin-neutral-200oklch(0.929 0.004 250)Neutral ramp, step 200.no component
--opsin-neutral-300oklch(0.871 0.005 250)Neutral ramp, step 300.no component
--opsin-neutral-400oklch(0.708 0.008 250)Neutral ramp, step 400.no component
--opsin-neutral-500oklch(0.556 0.009 250)Neutral ramp, step 500.no component
--opsin-neutral-600oklch(0.439 0.009 250)Neutral ramp, step 600.no component
--opsin-neutral-700oklch(0.371 0.008 250)Neutral ramp, step 700.no component
--opsin-neutral-800oklch(0.269 0.007 250)Neutral ramp, step 800.no component
--opsin-neutral-900oklch(0.205 0.006 250)Neutral ramp, step 900.no component
--opsin-neutral-950oklch(0.145 0.005 250)Neutral ramp, step 950.no component
--opsin-neutral-1000oklch(0 0 0)Neutral ramp, step 1000.no component
--opsin-chrome-backgroundoklch(0.985 0.002 250)Chrome role background, resolved to the ladder step it names.no component
--opsin-chrome-foregroundoklch(0.205 0.006 250)Chrome role foreground, resolved to the ladder step it names.no component
--opsin-chrome-cardoklch(1 0 0)Chrome role card, resolved to the ladder step it names.no component
--opsin-chrome-card-foregroundoklch(0.205 0.006 250)Chrome role cardForeground, resolved to the ladder step it names.no component
--opsin-chrome-mutedoklch(0.967 0.003 250)Chrome role muted, resolved to the ladder step it names.no component
--opsin-chrome-muted-foregroundoklch(0.439 0.009 250)Chrome role mutedForeground, resolved to the ladder step it names.no component
--opsin-chrome-borderoklch(0.556 0.009 250)Chrome role border, resolved to the ladder step it names.no component
--opsin-chrome-inputoklch(0.556 0.009 250)Chrome role input, resolved to the ladder step it names.no component
--opsin-chrome-ringoklch(0.55 0.13 250)Chrome role ring, resolved to the ladder step it names.no component
--opsin-chrome-primaryoklch(0.51 0.135 250)Chrome role primary, resolved to the ladder step it names.no component
--opsin-chrome-primary-foregroundoklch(0.99 0 0)Chrome role primaryForeground, resolved to the ladder step it names.no component
--opsin-category-sleep-50oklch(0.972 0.013 275)Sleep ramp, step 50.no component
--opsin-category-sleep-100oklch(0.941 0.028 275)Sleep ramp, step 100.no component
--opsin-category-sleep-200oklch(0.884 0.057 275)Sleep ramp, step 200.no component
--opsin-category-sleep-300oklch(0.806 0.092 275)Sleep ramp, step 300.no component
--opsin-category-sleep-400oklch(0.714 0.123 275)Sleep ramp, step 400.no component
--opsin-category-sleep-500oklch(0.622 0.14 275)Sleep ramp, step 500.no component
--opsin-category-sleep-600oklch(0.541 0.136 275)Sleep ramp, step 600.no component
--opsin-category-sleep-700oklch(0.452 0.123 275)Sleep ramp, step 700.no component
--opsin-category-sleep-800oklch(0.362 0.104 275)Sleep ramp, step 800.no component
--opsin-category-sleep-900oklch(0.276 0.081 275)Sleep ramp, step 900.no component
--opsin-category-sleep-950oklch(0.208 0.062 275)Sleep ramp, step 950.no component
--opsin-category-sleep-surfaceoklch(0.972 0.013 275)The tinted background a component in this ramp sits on.no component
--opsin-category-sleep-lineoklch(0.541 0.136 275)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.trend-sparkline
--opsin-category-sleep-inkoklch(0.362 0.104 275)Text and text-sized icons on `surface`. Clears the text floor against `surface`.metric-tile, range-bar, result-card, score-dial
--opsin-category-sleep-accentoklch(0.58 0.14 275)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.log-sheet
--opsin-category-heart-50oklch(0.972 0.014 15)Heart ramp, step 50.no component
--opsin-category-heart-100oklch(0.941 0.03 15)Heart ramp, step 100.no component
--opsin-category-heart-200oklch(0.884 0.062 15)Heart ramp, step 200.no component
--opsin-category-heart-300oklch(0.806 0.112 15)Heart ramp, step 300.no component
--opsin-category-heart-400oklch(0.714 0.15 15)Heart ramp, step 400.no component
--opsin-category-heart-500oklch(0.622 0.17 15)Heart ramp, step 500.no component
--opsin-category-heart-600oklch(0.541 0.165 15)Heart ramp, step 600.no component
--opsin-category-heart-700oklch(0.452 0.15 15)Heart ramp, step 700.no component
--opsin-category-heart-800oklch(0.362 0.126 15)Heart ramp, step 800.no component
--opsin-category-heart-900oklch(0.276 0.099 15)Heart ramp, step 900.no component
--opsin-category-heart-950oklch(0.208 0.075 15)Heart ramp, step 950.no component
--opsin-category-heart-surfaceoklch(0.972 0.014 15)The tinted background a component in this ramp sits on.no component
--opsin-category-heart-lineoklch(0.541 0.165 15)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.trend-sparkline
--opsin-category-heart-inkoklch(0.362 0.126 15)Text and text-sized icons on `surface`. Clears the text floor against `surface`.metric-tile, range-bar, result-card, score-dial
--opsin-category-heart-accentoklch(0.62 0.17 15)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.log-sheet
--opsin-category-activity-50oklch(0.972 0.022 140)Activity ramp, step 50.no component
--opsin-category-activity-100oklch(0.941 0.038 140)Activity ramp, step 100.no component
--opsin-category-activity-200oklch(0.884 0.067 140)Activity ramp, step 200.no component
--opsin-category-activity-300oklch(0.806 0.106 140)Activity ramp, step 300.no component
--opsin-category-activity-400oklch(0.714 0.141 140)Activity ramp, step 400.no component
--opsin-category-activity-500oklch(0.622 0.16 140)Activity ramp, step 500.no component
--opsin-category-activity-600oklch(0.541 0.155 140)Activity ramp, step 600.no component
--opsin-category-activity-700oklch(0.452 0.141 140)Activity ramp, step 700.no component
--opsin-category-activity-800oklch(0.362 0.118 140)Activity ramp, step 800.no component
--opsin-category-activity-900oklch(0.276 0.092 140)Activity ramp, step 900.no component
--opsin-category-activity-950oklch(0.208 0.07 140)Activity ramp, step 950.no component
--opsin-category-activity-surfaceoklch(0.972 0.022 140)The tinted background a component in this ramp sits on.no component
--opsin-category-activity-lineoklch(0.541 0.155 140)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.trend-sparkline
--opsin-category-activity-inkoklch(0.362 0.118 140)Text and text-sized icons on `surface`. Clears the text floor against `surface`.metric-tile, range-bar, result-card, score-dial
--opsin-category-activity-accentoklch(0.7 0.16 140)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.log-sheet
--opsin-category-nutrition-50oklch(0.972 0.02 70)Nutrition ramp, step 50.no component
--opsin-category-nutrition-100oklch(0.941 0.034 70)Nutrition ramp, step 100.no component
--opsin-category-nutrition-200oklch(0.884 0.059 70)Nutrition ramp, step 200.no component
--opsin-category-nutrition-300oklch(0.806 0.092 70)Nutrition ramp, step 300.no component
--opsin-category-nutrition-400oklch(0.714 0.123 70)Nutrition ramp, step 400.no component
--opsin-category-nutrition-500oklch(0.622 0.135 70)Nutrition ramp, step 500.no component
--opsin-category-nutrition-600oklch(0.541 0.118 70)Nutrition ramp, step 600.no component
--opsin-category-nutrition-700oklch(0.452 0.099 70)Nutrition ramp, step 700.no component
--opsin-category-nutrition-800oklch(0.362 0.08 70)Nutrition ramp, step 800.no component
--opsin-category-nutrition-900oklch(0.276 0.062 70)Nutrition ramp, step 900.no component
--opsin-category-nutrition-950oklch(0.208 0.049 70)Nutrition ramp, step 950.no component
--opsin-category-nutrition-surfaceoklch(0.972 0.02 70)The tinted background a component in this ramp sits on.no component
--opsin-category-nutrition-lineoklch(0.541 0.118 70)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.trend-sparkline
--opsin-category-nutrition-inkoklch(0.362 0.08 70)Text and text-sized icons on `surface`. Clears the text floor against `surface`.metric-tile, range-bar, result-card, score-dial
--opsin-category-nutrition-accentoklch(0.75 0.14 70)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.log-sheet
--opsin-category-mind-50oklch(0.972 0.018 310)Mind ramp, step 50.no component
--opsin-category-mind-100oklch(0.941 0.034 310)Mind ramp, step 100.no component
--opsin-category-mind-200oklch(0.884 0.059 310)Mind ramp, step 200.no component
--opsin-category-mind-300oklch(0.806 0.092 310)Mind ramp, step 300.no component
--opsin-category-mind-400oklch(0.714 0.123 310)Mind ramp, step 400.no component
--opsin-category-mind-500oklch(0.622 0.14 310)Mind ramp, step 500.no component
--opsin-category-mind-600oklch(0.541 0.136 310)Mind ramp, step 600.no component
--opsin-category-mind-700oklch(0.452 0.123 310)Mind ramp, step 700.no component
--opsin-category-mind-800oklch(0.362 0.104 310)Mind ramp, step 800.no component
--opsin-category-mind-900oklch(0.276 0.081 310)Mind ramp, step 900.no component
--opsin-category-mind-950oklch(0.208 0.062 310)Mind ramp, step 950.no component
--opsin-category-mind-surfaceoklch(0.972 0.018 310)The tinted background a component in this ramp sits on.no component
--opsin-category-mind-lineoklch(0.541 0.136 310)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.trend-sparkline
--opsin-category-mind-inkoklch(0.362 0.104 310)Text and text-sized icons on `surface`. Clears the text floor against `surface`.metric-tile, range-bar, result-card, score-dial
--opsin-category-mind-accentoklch(0.62 0.14 310)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.log-sheet
--opsin-category-labs-50oklch(0.972 0.015 210)Labs ramp, step 50.no component
--opsin-category-labs-100oklch(0.941 0.026 210)Labs ramp, step 100.no component
--opsin-category-labs-200oklch(0.884 0.046 210)Labs ramp, step 200.no component
--opsin-category-labs-300oklch(0.806 0.073 210)Labs ramp, step 300.no component
--opsin-category-labs-400oklch(0.714 0.097 210)Labs ramp, step 400.no component
--opsin-category-labs-500oklch(0.622 0.108 210)Labs ramp, step 500.no component
--opsin-category-labs-600oklch(0.541 0.094 210)Labs ramp, step 600.no component
--opsin-category-labs-700oklch(0.452 0.079 210)Labs ramp, step 700.no component
--opsin-category-labs-800oklch(0.362 0.063 210)Labs ramp, step 800.no component
--opsin-category-labs-900oklch(0.276 0.049 210)Labs ramp, step 900.no component
--opsin-category-labs-950oklch(0.208 0.038 210)Labs ramp, step 950.no component
--opsin-category-labs-surfaceoklch(0.972 0.015 210)The tinted background a component in this ramp sits on.no component
--opsin-category-labs-lineoklch(0.541 0.094 210)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.trend-sparkline
--opsin-category-labs-inkoklch(0.362 0.063 210)Text and text-sized icons on `surface`. Clears the text floor against `surface`.metric-tile, range-bar, result-card, score-dial
--opsin-category-labs-accentoklch(0.6 0.11 210)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.log-sheet
--opsin-status-steady-50oklch(0.972 0.015 178)Steady ramp, step 50.no component
--opsin-status-steady-100oklch(0.941 0.026 178)Steady ramp, step 100.no component
--opsin-status-steady-200oklch(0.884 0.046 178)Steady ramp, step 200.no component
--opsin-status-steady-300oklch(0.806 0.073 178)Steady ramp, step 300.no component
--opsin-status-steady-400oklch(0.714 0.097 178)Steady ramp, step 400.no component
--opsin-status-steady-500oklch(0.622 0.11 178)Steady ramp, step 500.no component
--opsin-status-steady-600oklch(0.541 0.1 178)Steady ramp, step 600.no component
--opsin-status-steady-700oklch(0.452 0.084 178)Steady ramp, step 700.no component
--opsin-status-steady-800oklch(0.362 0.067 178)Steady ramp, step 800.no component
--opsin-status-steady-900oklch(0.276 0.052 178)Steady ramp, step 900.no component
--opsin-status-steady-950oklch(0.208 0.041 178)Steady ramp, step 950.no component
--opsin-status-steady-surfaceoklch(0.972 0.015 178)The tinted background a component in this ramp sits on.alert-banner, status-pill
--opsin-status-steady-lineoklch(0.541 0.1 178)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.alert-banner, range-bar, range-legend, status-pill
--opsin-status-steady-inkoklch(0.362 0.067 178)Text and text-sized icons on `surface`. Clears the text floor against `surface`.alert-banner, score-dial, status-pill
--opsin-status-steady-accentoklch(0.62 0.11 178)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.score-dial
--opsin-status-watch-50oklch(0.972 0.02 82)Watch ramp, step 50.no component
--opsin-status-watch-100oklch(0.941 0.034 82)Watch ramp, step 100.no component
--opsin-status-watch-200oklch(0.884 0.059 82)Watch ramp, step 200.no component
--opsin-status-watch-300oklch(0.806 0.092 82)Watch ramp, step 300.no component
--opsin-status-watch-400oklch(0.714 0.123 82)Watch ramp, step 400.no component
--opsin-status-watch-500oklch(0.622 0.129 82)Watch ramp, step 500.no component
--opsin-status-watch-600oklch(0.541 0.112 82)Watch ramp, step 600.no component
--opsin-status-watch-700oklch(0.452 0.094 82)Watch ramp, step 700.no component
--opsin-status-watch-800oklch(0.362 0.076 82)Watch ramp, step 800.no component
--opsin-status-watch-900oklch(0.276 0.059 82)Watch ramp, step 900.no component
--opsin-status-watch-950oklch(0.208 0.046 82)Watch ramp, step 950.no component
--opsin-status-watch-surfaceoklch(0.972 0.02 82)The tinted background a component in this ramp sits on.alert-banner, status-pill
--opsin-status-watch-lineoklch(0.541 0.112 82)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.alert-banner, range-bar, range-legend, status-pill
--opsin-status-watch-inkoklch(0.362 0.076 82)Text and text-sized icons on `surface`. Clears the text floor against `surface`.alert-banner, score-dial, status-pill
--opsin-status-watch-accentoklch(0.60 0.123 82)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.score-dial
--opsin-status-attention-50oklch(0.972 0.015 45)Needs attention ramp, step 50.no component
--opsin-status-attention-100oklch(0.941 0.032 45)Needs attention ramp, step 100.no component
--opsin-status-attention-200oklch(0.884 0.063 45)Needs attention ramp, step 200.no component
--opsin-status-attention-300oklch(0.806 0.099 45)Needs attention ramp, step 300.no component
--opsin-status-attention-400oklch(0.714 0.132 45)Needs attention ramp, step 400.no component
--opsin-status-attention-500oklch(0.622 0.15 45)Needs attention ramp, step 500.no component
--opsin-status-attention-600oklch(0.541 0.145 45)Needs attention ramp, step 600.no component
--opsin-status-attention-700oklch(0.452 0.13 45)Needs attention ramp, step 700.no component
--opsin-status-attention-800oklch(0.362 0.105 45)Needs attention ramp, step 800.no component
--opsin-status-attention-900oklch(0.276 0.082 45)Needs attention ramp, step 900.no component
--opsin-status-attention-950oklch(0.208 0.065 45)Needs attention ramp, step 950.no component
--opsin-status-attention-surfaceoklch(0.972 0.015 45)The tinted background a component in this ramp sits on.alert-banner, status-pill
--opsin-status-attention-lineoklch(0.541 0.145 45)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.alert-banner, range-bar, range-legend, status-pill
--opsin-status-attention-inkoklch(0.362 0.105 45)Text and text-sized icons on `surface`. Clears the text floor against `surface`.alert-banner, score-dial, status-pill
--opsin-status-attention-accentoklch(0.58 0.15 45)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.score-dial
--opsin-status-urgent-50oklch(0.972 0.014 27)Urgent ramp, step 50.no component
--opsin-status-urgent-100oklch(0.941 0.03 27)Urgent ramp, step 100.no component
--opsin-status-urgent-200oklch(0.884 0.061 27)Urgent ramp, step 200.no component
--opsin-status-urgent-300oklch(0.806 0.11 27)Urgent ramp, step 300.no component
--opsin-status-urgent-400oklch(0.714 0.176 27)Urgent ramp, step 400.no component
--opsin-status-urgent-500oklch(0.622 0.2 27)Urgent ramp, step 500.no component
--opsin-status-urgent-600oklch(0.541 0.194 27)Urgent ramp, step 600.no component
--opsin-status-urgent-700oklch(0.452 0.176 27)Urgent ramp, step 700.no component
--opsin-status-urgent-800oklch(0.362 0.148 27)Urgent ramp, step 800.no component
--opsin-status-urgent-900oklch(0.276 0.115 27)Urgent ramp, step 900.no component
--opsin-status-urgent-950oklch(0.208 0.088 27)Urgent ramp, step 950.no component
--opsin-status-urgent-surfaceoklch(0.972 0.014 27)The tinted background a component in this ramp sits on.alert-banner, status-pill
--opsin-status-urgent-lineoklch(0.541 0.194 27)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.alert-banner, range-bar, range-legend, status-pill
--opsin-status-urgent-inkoklch(0.362 0.148 27)Text and text-sized icons on `surface`. Clears the text floor against `surface`.alert-banner, score-dial, status-pill
--opsin-status-urgent-accentoklch(0.52 0.2 27)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.score-dial
--opsin-status-unknown-50oklch(0.972 0.003 250)Unknown ramp, step 50.no component
--opsin-status-unknown-100oklch(0.941 0.005 250)Unknown ramp, step 100.no component
--opsin-status-unknown-200oklch(0.884 0.008 250)Unknown ramp, step 200.no component
--opsin-status-unknown-300oklch(0.806 0.013 250)Unknown ramp, step 300.no component
--opsin-status-unknown-400oklch(0.714 0.018 250)Unknown ramp, step 400.no component
--opsin-status-unknown-500oklch(0.622 0.02 250)Unknown ramp, step 500.no component
--opsin-status-unknown-600oklch(0.541 0.019 250)Unknown ramp, step 600.no component
--opsin-status-unknown-700oklch(0.452 0.018 250)Unknown ramp, step 700.no component
--opsin-status-unknown-800oklch(0.362 0.015 250)Unknown ramp, step 800.no component
--opsin-status-unknown-900oklch(0.276 0.012 250)Unknown ramp, step 900.no component
--opsin-status-unknown-950oklch(0.208 0.009 250)Unknown ramp, step 950.no component
--opsin-status-unknown-surfaceoklch(0.972 0.003 250)The tinted background a component in this ramp sits on.no component
--opsin-status-unknown-lineoklch(0.541 0.019 250)The boundary, icon stroke or chart mark. The lightest value in the ramp that clears the non-text floor against `surface`.no component
--opsin-status-unknown-inkoklch(0.362 0.015 250)Text and text-sized icons on `surface`. Clears the text floor against `surface`.no component
--opsin-status-unknown-accentoklch(0.72 0.02 250)The identity fill: a bar fill, a dial track, a legend dot. Chosen for recognition, not for contrast. It must be bounded by `line` or labelled in `ink`; it is never the only thing that carries the meaning.no component

Accessibility impact

This page is the accessibility impact of the colour system, so what follows is the boundary rather than a restatement.

What is guaranteed. The gated set clears both models at the floor above, in both themes and in sRGB. The nightly run measures the whole gated set and fails on any pair that has dropped below its floor, so a regression is caught within a day rather than on the commit that introduced it. Advisory pairs are published as well, and one may sit below its floor: the number that do is printed beside the "failing" figure in the summary, so a clean total never hides a sub-floor accent or hairline. Exempt pairs are outside the floor's scope by design. The list of declared pairs, and which state each takes, is enumerated in Colour roles, and the measured results are published, dated, on Contrast conformance.

What is not guaranteed, and is yours.

  • Any pair you assemble that is not in the declared set.
  • Text over imagery, video, gradients or any translucent surface.
  • Text you have made semi-transparent.
  • Text rendered at a weight or size other than the one the row above assumes. Lc is size-aware and a thin 12px label is not the same problem as a 15px regular one.
  • forced-colors: active, where the operating system replaces the palette entirely and the only thing that survives is your markup.

What contrast does not fix. Meeting the floor does not make a colour-coded status accessible; it makes it visible. A reader who cannot distinguish the four hues gets no benefit from any of these numbers, which is why the four levels always ship with a word. See Colour independence.

  • Colour blindness has the redundancy requirement that contrast alone cannot satisfy.
  • The contrast floor covers the same problem when the background is not a known colour, and the worst-case composite that replaces it.
  • Contrast conformance is the dated, generated conformance record for the whole token set.

On this page