Deriving a theme from your brand
The one-colour workflow, what the engine does with your brand colour, and the six brand colours that make it argue with you.
Overview
The supported way to make opsinjs look like your product is to give it one colour. Not a palette, not a set of overrides. From that one colour the neutral system, the interactive system and the shape of the ramps are derived, and against it the two clinical axes are then checked for interference.
This page is about what happens at the boundary: which brand colours the engine takes cleanly, which ones it takes with a warning, and which ones it refuses. Being told no by a theme generator is a strange experience, so it is worth saying why it happens at all. A brand colour is a marketing decision. In a consumer health product it lands next to a number somebody may act on, and at that point it stops being only a marketing decision. The engine is where the two concerns are reconciled, and it reconciles them by treating the clinical axes as fixed and the brand as adjustable.
The thing this is most often confused with is restyling. Deriving a theme changes the palette. It does not change the density, the shape language or the type. Those are separate foundations, and Lyra and the docs chrome explains why this documentation site deliberately looks unlike the product it documents.
How it works
The workflow
Pick the colour
One value, in any CSS colour syntax. Use the colour your brand actually uses for a primary action, not the one at the top of the brand guidelines. The engine derives an interactive system from the colour you pick, and the accent that reads well on a poster is often too light to be a button.
Run the generator
Theme generator runs the eight stages described in How the engine works live, and prints the ramp with the sRGB gamut boundary marked and the APCA verdict for every pair it emits.
Read the verdict before the swatches
The generator will show you a palette whether or not it is happy. The verdict block is the part that matters: it names any pair that needed adjusting in stage 7 and any that could not be rescued. A theme with adjusted pairs is fine. A theme with failed pairs is not shippable and the generator will say so.
Check the interference report
Separately from contrast, the generator reports how close your brand hue sits to the four status hues. This is not a pass or a fail. It is a judgement you have to make about your own product, and the failure it prevents is described under Brand colour sits in the status family below.
Copy the preset code, not the CSS
The generator emits a short opsinjs-* preset code as well as a block of custom
properties. Prefer the code: it is versioned, it round-trips back into the
generator, and it can be re-derived when the engine improves. The CSS block is
a snapshot and will not benefit from a fix to stage 4.
Presets documents the format.
Gate it in CI
A theme is a thing that drifts. The intent is that the same contrast maths runs in your pipeline, so that a designer nudging a hex value six months from now fails a build instead of shipping an unreadable result card. Validating your theme is the current state of that.
What is derived, and what is not
| Derived from your brand colour | Fixed, regardless of your brand |
|---|---|
--primary and its foreground | the four status ramps |
| the interactive/focus ring | the six category ramps |
| the neutral ramp's hue cast | the lightness ladder |
| chart and accent defaults | the contrast floor |
The clinical axes are fixed because they are not decoration. A product whose "urgent" is purple because purple is on brand has repurposed a safety signal for identity, and the reader who has used any other health product in their life will read it wrong. The one supported change to the status axis is documented, reluctantly, on Status palettes.
Using it
The six brand colours that cause trouble
These are the recurring cases, in rough order of how often they come up. None of them is a bug; each is the engine surfacing a real conflict early.
1. A yellow, lime or chartreuse brand (roughly hue 75 to 130). Available
chroma at those hues collapses at low lightness. The gamut boundary sits high
and narrow, so the dark end of the ramp desaturates hard and your ink step
will look grey rather than branded. There is no fix inside the engine, because
lightening the ink to keep the chroma would break the contrast floor. The
workable answer is to let the brand own --primary and the line role and let
ink be neutral. Most brands in this family already do this in print without
having noticed.
2. The brand sits in the status family (hues near 25, 55, 85 or 150). The
engine will still derive a perfectly good theme, and the generator will still
warn you, because the failure is semantic rather than numeric: a primary button
in the same red as urgent teaches the reader that red means "press me" on one
screen and "act now" on the next. If the brand cannot move, move the usage.
Keep the brand out of any surface that reports a result, and rely on the fact
that a status surface always carries a word and an icon.
3. A very low-chroma brand, such as greys, near-blacks and warm off-whites. Nothing breaks. The observation people make is that the status colours now dominate the screen. That is correct and it is the right outcome: in a health product the verdict should out-shout the identity. If it looks wrong, the problem is usually that too many things on the screen have a status at all.
4. Two brands, or a white-label product. Only one colour may own
--primary. A second brand becomes a category-axis-style identity token, a
logo, or nothing. Two primaries produce a screen where the reader cannot tell
which coloured control is the action.
5. A brand supplied in Display-P3 or from a print reference. Accepted, and the sRGB fallback is chroma-clamped at constant lightness and hue, which means it desaturates rather than shifting. On an sRGB display the theme will therefore be duller than the swatch the designer approved on their studio monitor, and that is a real, visible difference you should surface to them before launch rather than after. See Gamut and Display-P3.
6. A brand that is nearly white or nearly black. These carry almost no hue information at all, so the derived ramps are effectively neutral and the theme comes out looking like the default. That is not a failure, but it usually means the colour handed over was a background rather than an accent. Ask for the action colour.
Do
Hand the engine the brand's primary action colour, accept the adjusted L
that stage 7 returns for the foreground roles, and keep the preset code in
version control next to the app.
Don’t
Take the generated CSS block, hand-edit three values back to the exact brand hex "because the pink was off", and commit it. Every hand-edit is a pair that is no longer measured, and there is nothing in the file to say which three.
When you genuinely need to override one value
Do it in tokens/color.json and regenerate, never in the emitted CSS. The
generated file carries a header saying so, pnpm check:generated fails the
build when it has been edited by hand, and the reason for both is that an
edited generated file is a lie that survives a code review.
Adding your own tokens is the
supported extension path.
Tokens
| Token | What it controls | Used 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
The derivation is the only place in the system where an accessibility property can be lost silently, because it is the only place a value is chosen by somebody outside this repository. Three guarantees hold across it:
- The floor is not negotiable by theme. Stage 7 adjusts or fails; it never emits a below-floor pair, and there is no configuration flag that relaxes it.
- Both themes are derived together. A brand colour that works in light and fails in dark fails the derivation, rather than producing a light theme now and a dark-mode bug report later.
- Adjustments are reported, not hidden. When stage 7 moves a foreground, the generator says which role it moved and by how much, so a designer can decide to change the brand input instead of accepting a colour they did not choose.
What the derivation cannot do for you: it does not know what you put the colour on. A brand-coloured button placed on a photograph is outside the model, and belongs to The contrast floor.
Related
- How the engine works has the eight stages, with the failure mode of each stated separately.
- Status palettes explains why the four clinical ramps are excluded from derivation, and what to do if your regulator disagrees.
- Category palettes is the supported way to add an identity colour for a measurement we did not anticipate.
Colour roles
The four roles every colour token plays are surface, line, ink and accent. A contrast guarantee attaches to a pair, never to a colour.
Gamut and Display-P3
sRGB is the baseline, Display-P3 is a chroma escalation behind a media query, and the guard has to be a media query rather than a feature query.