Skip to content
Start
Live · free to use

Umber

A tool for picking brand colors that actually work together. It checks that text stays readable on every color while it builds the palette, so you don't find out a color fails halfway through a project.

Palette generation with the contrast math built into the generator.

Open umber-colour.vercel.app

OKLCH

Generation space, chosen over HSL

umber-colour.vercel.app
Umber in use

7:1

Body text contrast floor, enforced in the generator

50

Regeneration rounds locks were verified over

6

Schemes, from analogous to tetradic

01 FM · The problem

Why we built it.

Palette tools produce a row of swatches and leave the hard part to you. Choosing five colors you like is the easy part. The hard part comes three days into a build, when the accent can't carry text at a readable contrast, two of the five look the same, and the set falls apart in dark mode.

You end up with a list of hex codes, and all the work of turning them into an interface is still ahead of you.

02 FM · The approach

The decisions we made, and why.

This part gets into the technical detail. If that isn't your thing, the short version is at the top of the page.

01

We generate in OKLCH, because HSL lightness misleads

HSL lightness is a poor match for perception. Pure blue and pure yellow are both 'lightness 50%' in HSL, yet they look worlds apart. A generator built on HSL produces palettes that are balanced on paper and visibly uneven. Generating in a perceptual space means the numbers describing a palette match what you see.

02

Contrast rules run inside generation, rather than as a warning afterwards

We clamp every derived tone as it's produced: body text at 7:1 or above, secondary text at 4.5:1 or above. Every palette the generator emits already passes, so it's ready to use the moment it appears. The constraint sits upstream of the output instead of a validator bolted to the end.

03

We show the colors in use, not just the colors

The preview renders real interface, including buttons, chips, cards, menus, and inputs, through a role engine that assigns the palette the way a real interface would. A color card leaves you guessing whether a palette works. A button, a disabled state, and a focus ring tell you immediately.

04

The app wears the palette you're building

One surface color and one accent, with every minor tone derived under the clamps, are applied to the tool's own interface. If a palette is unusable, you find out because the app you're using becomes hard to use, which is a far more honest test than looking at swatches.

03 FM · What testing found

What we only learned by testing.

Every item below is a real problem we found by measuring, and each one has a fix behind it. We share them because they tell you more about how we work than the features do.

01

We wrote our own color extractor

Off-the-shelf tools return the colors but not where each one came from, and we needed every brand color traced back to the artwork. So we wrote a median cut, and every extracted color now points at the pixels it came from. A color you can trace is a measurement, where an untraced one is only an assertion.

02

We verified locks over fifty regeneration rounds

A locked swatch surviving one regeneration proves very little, because drift shows up over repetition. So we checked fifty rounds. Inserting a color interpolates rather than randomizing, so adding a swatch leaves the ones you've already chosen where you put them.

03

The role vocabulary is where the bugs live

Two role tokens that differ by one word, accent-as-text and text-on-accent, were swapped in a downstream consumer, and labels vanished in dark mode. The palette was correct, and the assignment was wrong. Color systems fail at the mapping far more often than at the math.

04 FM · Built with

Built with, and still open

  • TypeScript
  • Vite
  • OKLCH
  • Canvas
Still open

Nothing outstanding right now. It does what we built it to do, and we've described its known limits above.