Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

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

ContrastRatio(a, b) · PassesAA(text, ground) · ReadableOn(colour, ground) · ToOklch(hex) · FromOklch(l, c, h) · Mix(a, b, amount) · SuggestPalettes(base)

`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.

preview2 examples compiled by CIuikitcolouraccessibility

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.

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

Signature#

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#

Why 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?#

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.

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#

SuggestPalettes(base) answers four palettes, each with a Light and a Dark mode, each mode a Ground, Surface, Text, MutedText, Accent and OnAccent:

palettewhat it is
Quietthe base's own hue, kept low: a tinted paper, ink of the same family, the base as the accent
Base as accenta neutral ground with no tint, and the base as the only colour on it
Complementthe base tints the ground and the text; the accent is the colour opposite it
Deepthe 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).

Examples#

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));
    }
  }
}
[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#

Related

theme tokens

A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can…

Designing an app that looks designed — direction, type, colour, composition and motion

How to make an Osy# app look like somebody designed it rather than like the default: commit to one visual direction…

style props

Inside a `variants` block, each `Name = value` is a style prop from a fixed vocabulary the renderer maps to CSS — paint…