Umber
Palette generation with the contrast maths built into the generator.

OKLCH
Generation space, not HSL
7:1
Body text contrast floor, enforced in the generator
50
Regeneration rounds locks were verified over
6
Schemes, from analogous to tetradic
Why this exists at all.
Palette tools produce a row of swatches and leave the hard part to you. The hard part is not choosing five colours you like. It is discovering, three days into a build, that the accent you picked cannot legally carry text, that two of the five are indistinguishable at a glance, and that the whole set collapses in dark mode.
So the output is a list of hexes and the work of turning that into an interface remains entirely undone.
The decisions, and what they rule out.
Generate in OKLCH, because HSL lightness is a lie
HSL lightness is not perceptual. Pure blue and pure yellow are both 'lightness 50%' in HSL and are nowhere near each other to a human eye. Any generator built on HSL produces palettes whose members are supposedly balanced and visibly are not. Generating in a perceptual space means the numbers describing a palette match what a person sees.
Contrast clamps run inside generation, not as a warning afterwards
Every derived tone is clamped as it is produced: body text at or above 7:1, secondary at or above 4.5:1. The generator cannot emit a palette that fails, so there is no report to read and no failing state to correct. The constraint is upstream of the output rather than a validator bolted to the end.
Show the colours used, not the colours
The preview renders real interface, buttons, chips, cards, menus, inputs, through a role engine that assigns the palette the way an interface actually would. A colour card tells you nothing about whether a palette works; a button, a disabled state and a focus ring tell you immediately.
The application wears the palette you are building
One surface colour, one accent, every minor tone derived under the clamps: applied to the tool's own chrome. If a palette is unusable you find out because the thing you are using becomes unusable, which is a considerably more honest test than looking at swatches.
The things thinking about it did not catch.
Every item below is a real defect, found by measurement, with a commit behind it. They are here because the failures are more informative than the features.
The colour extractor had to be written, not imported
Off-the-shelf extraction returns colours but not the geometry. No way to say where in the image each colour came from. A median cut was written instead, so every extracted colour can point at the pixels it came from. A brand colour that cannot be traced back to the artwork is an assertion, not a measurement.
Locks were verified over fifty regeneration rounds
A locked swatch surviving one regeneration proves nothing; drift shows up over repetition. Fifty rounds, checked. Inserting a colour interpolates rather than randomising, so adding a swatch does not silently rearrange the ones you had already decided on.
The role vocabulary is where the bugs live
Two role tokens differing by one word, accent-as-text versus text-on-accent, were transposed in a downstream consumer, and the result was invisible labels in dark mode. The palette was correct; the assignment was not. Colour systems fail at the mapping far more often than at the maths.
- TypeScript
- Vite
- OKLCH
- Canvas
Nothing outstanding. It does what it was built to do, and the known limits are described above rather than parked here.
Free, no account, running right now.
umber-colour.vercel.app