You pick a blue you like. Then a green to go beside it, a warm grey for backgrounds, an orange for the buttons, and something for links. Five separate acts of taste. On the screen they refuse to sit together: the green is louder than the blue, the grey looks dead next to both, and the orange button vanishes into the background at exactly the size where it matters.
Nothing is wrong with any of the five colours on its own. What is missing is a relationship between them. A palette that holds together is almost never five choices. It is one choice, and then arithmetic: a hue, plus a small set of rules that derive everything else from it.
The arithmetic is short enough to write out in full, which is what the rest of this article does. Hue, saturation and lightness are three numbers. A palette is what you get when you move one of those numbers at a time and leave the other two alone. There is already a piece on this site about choosing a colour palette without learning colour theory, which is the taste side of the question. This one is the maths side.
One colour in
Type a hex, pick a scheme, and read the explanation afterwards if the result is not doing what you expected. The whole thing is a color palette generator that runs inside the tab: no colour library, no network request, nothing uploaded anywhere. Every swatch below carries its hex, its HSL and its measured contrast against both white and black, and clicking one copies the hex.
Palette generator
Give it one colour and it works out a nine step ramp, a surface, an accent and two tinted neutrals, with the contrast figures for each. All the maths runs in this browser tab, nothing is uploaded.
Hex, rgb() or hsl() all work.
Roughly 60% surface, 30% base and 10% accent is the split most interfaces settle on.
A heading in your palette
Body copy sits on the near white, the base colour carries the one action that matters, and the accent turns up once.
The same palette after dark
On the near black the accent does the work, because the base is often too heavy to sit on a dark ground.
Click any swatch to copy its hex. Contrast follows WCAG 2.1, where 4.5 or more is safe for body text and 3 or more for large text and interface edges.
Three numbers, one cylinder
HSL describes a colour as a position in a cylinder. Hue is the angle around the rim, 0 to 360 degrees, running red, yellow, green, cyan, blue, magenta and back to red. Saturation is the distance out from the grey axis in the middle, 0 to 100 percent. Lightness is the height, 0 percent at the black floor and 100 percent at the white ceiling, with the most intense version of any hue sitting at 50 percent.
That is the whole reason to work in HSL rather than in RGB. In RGB no single channel corresponds to anything a designer wants to change: making a colour lighter means moving all three, in a ratio you have to work out first, and getting that ratio slightly wrong drifts the hue. In HSL, lighter is one number going up.
Here is a real pair, so the rotation is not abstract. The blue used throughout this site sits at hue 227, and its complement is what you get by adding 180 degrees to that angle and touching nothing else.
base #3B66FF hsl(227, 100%, 62%)
rotate +180 deg hue 227 becomes hue 47
accent #FFD43B hsl( 47, 100%, 62%)
Saturation and lightness are identical in both rows. The only thing that moved was the angle, and the result is a yellow that belongs to the blue in a way no separately chosen yellow ever quite does. The input field accepts that colour written any of the ways you are likely to have it already: #3B66FF, 3b66ff, the three digit form #f00, an rgb() string with commas or spaces (an alpha value is ignored), or an hsl() string with or without the deg suffix.
Moving one axis at a time
Move lightness alone and you get a ramp: the same colour from a near white tint down to a deep shade, all of it recognisably one family because the hue and the saturation never changed. This is the ladder every design system ships, usually labelled 100 at the top and 900 at the bottom. The generator builds nine of these steps at fixed stops running from 95 percent lightness down to 15 percent.
Move saturation alone and you get the same colour, quieter. This is the axis that decides whether a page is comfortable to sit in front of. A colour at 100 percent saturation has one RGB channel pinned at its maximum and one at its minimum, so there is no headroom left in it. Over a large area that causes two problems: an accent works by being more intense than its surroundings, and nothing beats a background already sitting at the edge of the gamut; and the eye adapts to a strong colour field within seconds, so anything laid on top starts drifting towards the opposite hue. Hence a desaturated surface rather than the base itself.
Move hue alone and you get the schemes. Analogous rotates a little way in each direction and stays in the same emotional register. Complementary goes to the far side of the wheel and gives you maximum separation. Triad and split complementary sit in between, with the split version pulling back from the exact opposite to take some of the harshness out of it. Monochrome does not rotate at all, which leaves it with no hue to work with, so the generator moves its accent 26 lightness points instead of rotating it. That is a compromise, and the tool shows you the result rather than pretending otherwise.
The set the generator derives
From one base colour and one scheme, the output is a ramp of nine steps, a surface, an accent, and two tinted neutrals. Analogous, triad and split complementary produce a second accent because those schemes define two angles; monochrome and complementary define one, so they produce one.
One detail is worth knowing before it surprises you. The ramp stops are absolute rather than relative to your base, so a base at 62 percent lightness falls between two steps and is not itself a step. The generator flags the nearest one instead of bending the ramp to fit, because a ramp whose spacing shifts with every base is useless as a system: you could not swap the brand colour later and expect the same nine tokens to behave the same way.
Why pure grey looks unfinished
Mid grey is #808080, which is hsl(0, 0%, 50%): a hue of zero, a saturation of zero, and no connection to anything else on the page. Put it next to a near white that carries a trace of the base hue and the pure grey reads as a different material, as though it had been cut out of another design and pasted in.
The reason is that we read a page as a lit surface. Under one light, every material picks up some of the same colour, so a set of neutrals that all carry a trace of the same hue reads as one scene. Zero saturation says the opposite: this surface is lit by nothing. It is the difference between a wall photographed in a room and a wall sampled from a colour chart.
So the three neutrals here are tinted rather than grey. The surface sits at 94 percent lightness, the near white at 97 percent and the near black at 9 percent, and each takes the base saturation scaled down and capped, at 14, 10 and 16 respectively. Scaled down, not set: this matters, because it means a grey base still gives you a grey palette. Feed the tool #808080 and you do not get an invented hue back, which is the failure mode of every generator that hard codes a tint value.
Sixty, thirty, ten
Four colours in a list are not a design. Four colours in a proportion are. The old decorator’s ratio, 60 percent of the area to one colour, 30 percent to the second and 10 percent to the third, is the step that turns the output above into something you can actually build with: the surface takes the 60, the base takes the 30, the accent takes the 10.
The reason the ratio works is that it destroys the tie. Two colours at equal area have no figure and no ground, and the eye receives no instruction about where to look. Push one of them out to 60 percent and it becomes the ground by default; everything else is then read against it. The accent surviving at 10 percent is the only thing left that can act as a signal, which is exactly why it has to stay at 10. An accent applied to six different elements is not an accent any more, it is a third background. This is the same argument as the one behind white space in composition: the emphasis comes from the ratio, not from the ingredient.
The strip under the swatches shows that split at real proportions, and the two preview cards below it, one light and one dark, put the palette into a tag, a heading, a paragraph and two buttons, so you can see whether it survives contact with an interface. On screens narrower than 700 pixels the strip drops its inline labels, since none fits inside a 10 percent segment, and the sentence underneath carries the numbers instead.
Contrast is a measured number
The one part of this that is not taste is contrast, and it has a formula. Convert each sRGB channel back to linear light, weight the three channels and add them up to get relative luminance, then compare two luminances with a fixed ratio.
relative luminance
L = 0.2126 * R + 0.7152 * G + 0.0722 * B
R, G, B linearised from sRGB, each 0 to 1
contrast ratio
(L_lighter + 0.05) / (L_darker + 0.05)
range 1:1 (identical) up to 21:1 (black on white)
Two thresholds are worth memorising, both from WCAG 2.1. A ratio of 4.5:1 or more is safe for body text. A ratio of 3:1 or more is the bar for large text, meaning roughly 18pt, or 14pt when it is bold, and it is also the bar for the non-text parts of an interface: input borders, icons, the edge of a button. Below 3:1 you are guessing. The generator colours every contrast reading on that scale, green at 4.5 or more, amber at 3 or more, red below.
Each swatch is measured against white and against black, because you rarely know in advance which ground a colour will end up on. Most colours pass one test and fail the other. The blue above is unusual: #3B66FF comes out at 4.64:1 on white and 4.53:1 on black, so it clears the body text bar either way. Its complement does not. #FFD43B is 1.43:1 on white and 14.73:1 on black, which means it is a colour you can set black text on and nothing else. If you ever wondered why a bright yellow call to action with white lettering looks wrong, that is the number saying so, and the same measurement is what decides whether text stays readable over a photograph.
There is exactly one place where the tool overrules its own palette. The secondary button in each preview card is darkened or lightened in 2 point steps until it clears 4.5:1 against the card it sits on, so the preview never shows you unreadable text and calls it a demonstration. Everywhere else the palette is reported as it is, contrast failures included.
Where HSL lies to you
HSL is the right space for deriving a palette and the wrong space for judging one. Its lightness value is a geometric position in that cylinder, not a measurement of how much light the colour actually emits, so two colours can sit at identical lightness and be nowhere near each other in brightness.
Pure yellow is hsl(60, 100%, 50%) and pure blue is hsl(240, 100%, 50%). Same saturation, same lightness. Their relative luminances are 0.9278 and 0.0722, a factor of nearly thirteen. The reason is sitting in the formula above: yellow is red plus green at full strength, so it collects 0.2126 plus 0.7152, which is 0.9278 exactly. Blue collects the 0.0722 term and nothing else. The weights are not arbitrary, they follow how sensitive human vision is to each part of the spectrum, and they make the middle of the lightness axis a fiction.
Three consequences follow. A step labelled 500 is not a fixed perceived brightness across hues, so a yellow 500 and a blue 500 are not interchangeable in a component. You cannot rotate a hue and assume the contrast comes with it, which is why the generator measures the accent afresh rather than inheriting the base reading. And two colours picked at matching HSL lightness will not look like a matched pair, because one of them is doing most of the shouting.
What to do about it is simple enough. Read the measured number on each swatch rather than trusting the step label. Expect yellow, green and cyan hues to need a much darker step before text on them passes, and expect blue and violet hues to pass on white at a lightness where a yellow would not come close. If you genuinely need a ramp whose steps are perceptually even, that is what LAB and OKLCH exist for, and this tool deliberately does not go there: it stays in the one space that maps directly onto the numbers people actually type into a stylesheet.
What it refuses to do
Bad input is refused rather than guessed at. Type something the parser cannot read and the field turns red, the message tells you it is keeping the last valid colour, and the palette does not move. An empty field is not treated as an error, because clearing a field is the normal first step in retyping one. Leaving the field normalises whatever is in it back to the current base hex. An out of range hsl() is wrapped on the hue and clamped on saturation and lightness, so hsl(400 120% 150%) resolves rather than failing.
The rest of the omissions are deliberate. There is no alpha channel, so translucent overlays are outside its scope. There is no CMYK or LAB output, which means it will not tell you what any of this does on paper, and what changes between screen and print is a separate problem with its own arithmetic. It does not auto-fix the palette itself: if your base fails on white, it says so and leaves the decision to you, because silently shifting somebody’s brand colour is worse than reporting the failure. And nothing is saved between visits, which follows from the fact that nothing leaves the browser in the first place. Copy the CSS variables or the JSON before you close the tab.
Working from one decision
The point of deriving instead of picking is not that arithmetic has better taste than you do. It is that arithmetic is consistent, and consistency is most of what people mean when they call a palette designed. Nine ramp steps built from one hue always relate to each other. Nine steps chosen by eye across three sessions do not, and no amount of later adjusting fixes it, because the problem is not any individual colour.
So the decision that is left to you is a smaller one, and a better one: which hue, and how far around the wheel the accent should go. Everything after that is a rule you can restate in a sentence. Move lightness for the ramp, cut saturation for the neutrals, rotate hue for the accent, and split the area 60, 30 and 10. Check the two contrast thresholds before you commit to anything a visitor has to read.
Interfaces are the easy case, because you control every colour in them. Photographs are not: the colour arrives with the picture and has to be steered rather than specified, and colour grading in five moves covers that side of the same problem. The measurement is identical either way, and a palette you can defend with numbers is one you can hand to somebody else without a paragraph of explanation attached.