Colour

Building a palette from a single colour

Generators hand you colours of equal lightness, which is why the result looks like a chart. A usable interface palette is a ramp, one loud accent, and greys that are not quite grey.

Every palette generator I have used answers the wrong question. Hand one a colour and it returns a set that is pleasing to look at: same lightness, same saturation, evenly spaced around a wheel. On a page that set looks like a paint chart — loud buttons, unreadable text, and a flatness you can see but cannot name.

The mistake is treating a palette as a set of colours. It is a set of jobs: a page ground, something that sits on top of it, something that marks a border, something that carries body text, and one thing that has to be the colour strangers click.

Generated palettes fail at the same two things

Take a colour wheel and swing a compass around it. Every position you land on is a different hue, and every one is drawn at the same lightness and saturation, because that is what the wheel's geometry produces. What comes back is a set of colours with identical luminance.

That is fatal, and it is measurable. An orange at #E98035 and its complement, a blue at #359EE9, drawn at the same lightness and saturation, have a contrast ratio of 1.05:1 — about as close to invisible as a pair of colours gets. Those two are the pair a colour wheel calls most harmonious. Equal luminance is what the wheel optimises for and the one thing a page cannot survive.

The second failure is saturation. A colour at full saturation is vivid at every lightness, so the pale end of a generated set is not pale. It is a vivid colour faded with white — a pastel somebody chose rather than a surface that recedes.

A palette is a set of jobs

Before any choosing, the inventory. These are the jobs I have needed on every interface I have built, and the count beside each is what covers the job without padding the set.

RoleWhat it is forHow many
Surface rampPage ground, raised card, sunken well3
HairlineTable rules, card edges, input outlines2
Ink rampHeadings, body, muted caption, disabled label4
AccentLinks, primary button, the one thing to click1
Accent shadeHover and pressed states of the accent1
SemanticSuccess, warning, danger3, plus a tint for each
Focus ringKeyboard focus, visible on every surface1

Two hairline values rather than one, because a decorative divider and an input border are not the same object and only one of them carries a contrast requirement.

Almost every one of those values is the same hue at a different lightness. A palette is a ramp with a few exceptions bolted to the side of it, not a bag of colours that happen to co-exist.

One hue, stepped through lightness

Here is a ramp from a base hue of 25 degrees, which is an orange. Seven steps, lightness falling in roughly even strides, and the distance from grey rising to a small peak in the middle before it drops away again at both ends.

Lightness Hex Distance from grey Used for
98%#FBFAF92 of 255Page ground
93%#F1EDE98 of 255Raised card, striped table row
86%#E2DAD414 of 255Heading divider, code-block well
74%#C6BBB418 of 255Disabled control fill, chart gridline
56%#978D8716 of 255Muted label, placeholder, inactive icon
34%#5C565111 of 255Body text
14%#2723207 of 255Headings, and the label on an accent fill

Distance from grey is the spread between the widest and the narrowest colour channel, out of 255. Zero would be a perfect neutral. The accent, for comparison, sits at 180.

Read that third column top to bottom and the curve is the whole technique. The top step sits 2 out of 255 away from grey, a ground with a temperature rather than a colour. By the middle it is 18, and by the bottom it has come back to 7.

The accent is not on the ramp. At hue 25, lightness 56% and high saturation it lands on #E98035, 180 away from grey — ten times the ramp's widest step. That ratio, not the hue, is what makes it read as the interactive thing on the page.

Desaturate the ends

As you approach either end, take saturation down. A colour at full saturation and 96% lightness is a vivid orange faded with white, and across a full-width surface that tint is loud — the difference between an off-white that recedes and a pale orange that asks to be looked at.

A saturation figure on its own says nothing about how colourful a colour looks, because your eye reads the spread between the channels in absolute terms. That spread is what has to come down at the ends.

Greys that are nearly grey

Pure grey next to a coloured interface looks dead. Not wrong, dead. A mid neutral with no hue at all beside an orange accent reads like a screenshot of a page that failed to load.

Give the greys the brand hue at a fraction of its strength. The ramp above is hue 25 all the way down, the same hue as the accent, at a tenth of the distance from grey.

If you would rather think in hue offsets than in tint strength, keep the neutrals inside about eight degrees of the brand hue. At fifteen an off-white starts reading as a colour rather than a warm grey; at thirty it is unambiguously a colour and you have built a two-hue palette by accident.

The tint also buys a black that is not black. #000000 beside an orange interface reads as a hole punched through the page, where #272320 reads as the darkest step of the same material. The same goes for the top of the ramp: #FFFFFF is brighter than most interfaces need.

What harmony rules are good for

Complementary, triadic, split-complementary and the rest of that family are answers to a question about colour relationships, not about interfaces. Every colour a harmony rule returns has the same luminance, so every pair inside the set has a contrast ratio around 1:1. That is why a palette built from one looks like a chart: not because the colours clash, but because there is no hierarchy in it, and a page has to tell you at a glance what is a surface and what is a button.

Where the rules earn their keep is narrow: picking a second accent you will use twice. A badge for something new, one line on a chart, one state that differs from the normal state. The rule hands you a candidate far from your brand hue and not readily confused with it. Take it, then do the two things the rule did not. Pull its saturation down, and push its lightness away from the accent's, because a second accent at the accent's luminance is invisible. If the two ever share a legend and you have to look twice to tell them apart, you have one accent too many.

Check the text pairs last

Everything above is about how a page looks when nobody is reading it. The text pairs decide whether anyone can read it at all, and they get settled last, once the ramp and the accent have stopped moving, because a text colour's contrast depends on the surface it lands on.

The target is 4.5:1 for body text, and 3:1 for large text and for the visible edge of a control. The ratio comes out of relative luminance, which is worth writing down once because it explains why the near-white tint is not free:

for each channel c, as a fraction from 0 to 1:

  c = c / 12.92                      when c is at or below 0.04045
  c = ((c + 0.055) / 1.055) ^ 2.4    otherwise

L = 0.2126 R + 0.7152 G + 0.0722 B

ratio = (L lighter + 0.05) / (L darker + 0.05)

Green carries 0.7152 of the luminance and blue carries 0.0722, which is why those coefficients look arbitrary until you run them. Against the #FBFAF9 page ground, the ramp's own values come out like this.

Text on groundRatioVerdict
#272320 on #FBFAF915:1Headings and body, with room to spare
#5C5651 on #FBFAF96.9:1Body text, comfortable
#978D87 on #FBFAF93.1:1Large text and control edges only
#E2DAD4 on #FBFAF91.3:1Decoration, never a border that means something
#FFFFFF on #E980352.8:1Fails, and it is the filled-button default
#272320 on #E980355.6:1The same button with a dark label
#B45F22 on #FFFFFF4.6:1Passes as a link on a white ground
#B45F22 on #FBFAF94.4:1Fails, and only because of the tint

The last two rows are the same colour on two different whites. The tint in the neutral cost a few hundredths of a luminance point, and that was all it took.

That is why the ordering matters. #B45F22 on pure white is 4.6:1 and passes. On the #FBFAF9 page ground it is 4.4:1 and fails. Pick a link colour against a white canvas in a design tool and you will never see it.

The row I would stare at hardest is the filled button. White on #E98035 is 2.8:1 and fails, so the label most people reach for is the one they cannot use. The ramp's near-black on the same fill is 5.6:1.

One thing falls out of doing this last. A border needs 3:1 against its surface only when the border is what identifies the control, which #978D87 covers and the #E2DAD4 divider does not. The focus ring is not decoration, and has to clear 3:1 against every surface in the ramp.

Where the tools fit. The Palette Generator is where I work out a ramp like the one above: give it the base hue, ask for the steps, and read the hex values off the result. Nothing is uploaded — a hue goes in and hex comes back.

The Colour Studio is the other half, and it does the job that has to happen last. Put the page ground and the body colour in, read the ratio, then the accent and its label, until every pair you ship clears 4.5.

The rule I keep

If you take one thing: a palette is one hue moved along the lightness axis, plus one colour far more saturated than anything around it, plus a set of greys carrying a trace of that same hue. The harmony pairs and the semantic set are bolted on afterwards and should never outnumber the ramp.

The check you can run in two minutes is to screenshot your page, convert the screenshot to greyscale, and look at it. If the greyscale version is readable and hierarchical, with clear surfaces and an obvious button, the palette is working. If it is a flat grey rectangle with no hierarchy, you built a set of peers and the colour has been carrying weight it cannot hold. That one look catches the equal-luminance failure from the top of this piece and the contrast failures from the bottom of it.

Keep the ramp and the accent in a comment at the top of the stylesheet, with the hex value and the job beside each one. In six months the question will not be what colour the divider is. It will be which of the two near-whites was meant for a raised card.