# The colour kit — contrast, OKLCH, and palettes from one colour

> `Osysharp.Color` is colour maths in plain Osy#, for an app that lets people choose colours: read a hex however it is typed, work in OKLCH, measure contrast as WCAG does, nudge a colour until text in it can be read, and propose whole palettes — light and dark — from one base colour. Every palette it proposes passes WCAG AA.

<!-- id: ui-color-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/color-kit/ -->

## Summary        {#summary}
A theme's colours are fixed when an app is compiled. An app that lets a *person* choose colours — a brand colour in
a setup screen, a label's tint, a board of swatches — needs to do colour maths while it runs: is this text readable
on that ground, what is the same colour but lighter, what palette goes with this one. `Osysharp.Color` is that maths
as ordinary functions over hex strings, the same on the server and in the browser.

```osy syntax
app Atelier {
  use Osysharp.Color@0;
  model "model/**/*.osy";
}
```

## Signature      {#signature}
```osy syntax
using Osysharp.Color;

string NormalizeHex(string text)                    // "#abc" → "#AABBCC"; "" when it is not a colour
bool   IsHexColor(string text)

double ContrastRatio(string one, string other)      // WCAG 2: 1 (none) to 21 (black on white)
string ContrastText(string one, string other)       // "4.6 : 1"
bool   PassesAA(string text, string ground, bool large = false)    // 4.5, or 3 for large text
double Luminance(string hex)
bool   IsDark(string hex)
string InkOn(string ground)                         // black or white, whichever reads better
string ReadableOn(string colour, string ground, double ratio = 4.5)

Oklch  ToOklch(string hex)                          // .L 0–1, .C 0–~0.37, .H degrees
string FromOklch(double l, double c, double h)
string WithLightness(string hex, double lightness)
string WithChroma(string hex, double chroma)
string Rotated(string hex, double degrees)          // 180 is the complement
string Mix(string first, string second, double amount)

List<SuggestedPalette> SuggestPalettes(string baseColor)
bool   IsReadable(PaletteMode mode)
```

## Description    {#description}

### Why OKLCH?   {#oklch}
In OKLCH a colour is its lightness, its chroma (how colourful) and its hue. Equal steps in those numbers look like
equal steps, so "the same colour, lighter" keeps its hue — where doing the same on red, green and blue drifts toward
grey or toward a neighbouring hue. Everything in the kit that changes a colour changes it there.

`FromOklch` never answers a colour a screen cannot show: one that is too colourful for its lightness keeps its
lightness and hue and gives up chroma — the nearest colour of that lightness and hue that exists.

### Can this text be read?   {#contrast}
`ContrastRatio` is WCAG 2's: the ratio of the two colours' relative luminance. Ordinary text needs 4.5 (`PassesAA`),
large text and the outline of a control 3 (`large: true`).

`ReadableOn(colour, ground)` answers the colour made readable on the ground: the **same hue and chroma**, its
lightness moved away from the ground's only as far as the ratio needs. A colour that already passes comes back
unchanged; a ratio the pair cannot reach answers black or white.

```osy syntax
var label = ReadableOn(brand, "#FFFFFF");            // the brand colour, usable as text on white
var chip  = InkOn(brand);                            // what to write ON the brand colour
```

### Palettes from one colour   {#palettes}
`SuggestPalettes(base)` answers four palettes, each with a `Light` and a `Dark` mode, each mode a `Ground`,
`Surface`, `Text`, `MutedText`, `Accent` and `OnAccent`:

| palette | what it is |
|---|---|
| Quiet | the base's own hue, kept low: a tinted paper, ink of the same family, the base as the accent |
| Base as accent | a neutral ground with no tint, and the base as the only colour on it |
| Complement | the base tints the ground and the text; the accent is the colour opposite it |
| Deep | the base itself is the ground, dark; in the light mode it is the ink on a pale ground |

**Every palette it proposes can be read.** Each mode is built and then checked — text and quieter text against the
ground and the surface, the accent against the ground, the words on the accent — and whatever falls short is moved in
lightness until it passes. `IsReadable(mode)` is that check, for a mode you have changed yourself.

The role names are a theme's: Ground is `Bg`, Surface is `Surface`, Text is `OnSurface`, MutedText is `TextMuted`,
Accent is `Primary`, OnAccent is `OnPrimary` ([theme tokens](https://osysharp.com/reference/ui/theming/)).

## Examples       {#examples}
```osy title="a swatch that says whether it can carry text" test app=ui-color-kit
app Atelier {
  use Osysharp.Color@0;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Color;

[Composable]
component Swatch(string hex) {
  render {
    Row(gap: 2, align: Align.Center, bg: hex, color: InkOn(hex), px: 3, py: 2, rounded: Radius.Md) {
      Text(NormalizeHex(hex));
      Text(ContrastText(InkOn(hex), hex));
    }
  }
}
```

```osy title="the ink a swatch is given can be read on it" run app=ui-color-kit
[Test]
void the_ink_chosen_for_a_ground_passes_on_it() {
  Assert.Equal("#AABBCC", NormalizeHex("#abc"));
  Assert.True(PassesAA(InkOn("#0B1F25"), "#0B1F25"));
  Assert.True(PassesAA(InkOn("#F4F1EA"), "#F4F1EA"));
}
```

## See also       {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — a theme's tokens, light and dark, and `Palette.From` for a ramp fixed at compile time
- [Designing an app that looks designed — direction, type, colour, composition and motion](https://osysharp.com/reference/ui/design/) — choosing a look on purpose
