Colour themes that learn, and stay accessible

Let a designer point at colours. The theme learns their taste and can't learn to be unreadable.

1 A theme function, and a designer with opinions

One function turns a brand hue and seven constants per mode into a whole light and dark theme. The designer doesn't want to edit constants; they want to point at colours. And every text colour has to stay readable: WCAG AA, at least 4.5:1.

the theme code runs as a Handwired net

  • contrast failures (36 brand hues × 2 modes × 8 pairs) 0
  • furthest from the picks (8-bit units) –
  • net vs plain JS, different hex colours –

the designer's picks

light

dark

pair (at the brand hue)lightdark

2 Predict

The Garden shop designer picked a soft grey-green for light-mode text, #8a9a88: only 2.8:1 on their background. After learning, the light theme's text will be…

3 Pick, then learn

Choose a brand above or pick your own colours (the colour inputs work from the keyboard). Press Learn: fourteen constants move in the code, the whole theme follows, and contrast is checked across 36 brand hues after every step.

4 The foil: a generic model on the same picks

Fit a small neural net (hue and mode → all five colours, 4→16→15) to the same picks. It nails them, including the ones that are unreadable.

5 The diff you’d review

Learn first to see the diff.

Reach for Handwired when a designer's taste should set the numbers, but accessibility is a rule, not a suggestion.

How the rule is exact: every colour is built in linear RGB as a luminance Y plus a push along the brand hue inside the plane where luminance doesn't change, capped at the exact edge of the sRGB gamut. So each colour's WCAG luminance is exactly Y, and “contrast ≥ r” becomes a straight-line bound on Y, which the net applies with exact min/max operations (a clamp, i.e. a projection onto the allowed range). The net enforces 4.6:1, a little above AA's 4.5:1, to leave room for rounding to 8-bit hex; the counter then measures the real WCAG formula on the hex colours the browser paints. Large-text and UI pairs need only 3:1, so 4.6:1 covers them too. The generic model's outputs are clamped to [0, 1] before painting. Hue here is an angle in that luminance-neutral plane, not OKLCH hue. The preview is drawn as an image so that its deliberately failing colours (in the foil) don't make this page itself inaccessible; the table gives every ratio as text.

Loading Handwired…