Design & code

Choosing colours that work

Hex is fine for storing a colour and terrible for reasoning about one. Switching to HSL makes building a palette a matter of arithmetic.

· 8 min read

Most people pick colours by nudging a picker until something looks right, then paste the hex code and move on. That works for one colour. It falls apart the moment you need five that belong together, or a text colour that stays readable on a coloured background.

The fix is not better taste. It is using a colour format that lets you reason about what you are changing.

Three notations, one colour

#2563EB, rgb(37, 99, 235) and hsl(221, 83%, 53%) are the same blue. They differ only in how they describe it.

Hex is three bytes in base 16 — two digits each for red, green and blue, each running 00 to FF (0–255). It is compact, universally supported, and easy to copy. It is also completely opaque to human reasoning: nothing about #2563EB tells you it is a mid-bright blue, and if asked to make it 10% lighter you would have no idea which digits to touch.

RGB is the same three channels in decimal. Useful in code where you want to manipulate a single channel, but it inherits hex's core problem — the numbers describe how a screen produces the colour, not how a person perceives it. Making a colour lighter in RGB means raising all three channels by some non-obvious amount without shifting the hue.

HSL re-describes the same colour in terms humans actually think in:

Now "10% lighter" is a single number changed by 10. That is the whole argument for HSL.

Building a palette with arithmetic

Once colour is three independent dials, a coherent palette stops being guesswork. Pick one colour you like, read its HSL, then vary one dimension at a time.

For tints and shades — the light and dark variants of a single brand colour — hold hue and saturation fixed and step the lightness. Starting from hsl(221, 83%, 53%), lightness values of 95, 85, 70, 53, 40, 28 and 18 give you a seven-step ramp from a barely-there background tint to a near-black text colour, all unmistakably the same blue. This is exactly how most design systems generate their colour scales.

For a second, related colour, shift the hue and leave saturation and lightness alone. Roughly 30° away gives an analogous colour that blends quietly. 180° away gives the complementary — maximum contrast, useful for an accent that must stand out. 120° apart in both directions gives a triadic set.

There is one perceptual wrinkle worth knowing. HSL lightness is a mathematical construct, not a measure of how bright a colour looks. Yellow at 50% lightness appears far brighter than blue at 50% lightness, because the eye is much more sensitive to green wavelengths than to blue. So a palette built by holding lightness constant across very different hues will not look evenly weighted. Perceptually uniform spaces such as OKLCH exist precisely to fix this, and are now well supported in CSS — but HSL remains the pragmatic default, as long as you trust your eyes over the numbers when hues differ a lot.

Contrast is measurable, not a matter of opinion

"Is this readable?" has an actual answer. The Web Content Accessibility Guidelines define a contrast ratio between two colours, computed from their relative luminance, running from 1:1 (identical) to 21:1 (pure black on pure white).

ContentMinimum (AA)Enhanced (AAA)
Body text4.5:17:1
Large text (18pt+, or 14pt bold)3:14.5:1
UI components, icons, form borders3:1

These thresholds are not arbitrary aesthetics. They come from research on legibility for readers with low vision, and they matter far beyond that group: everyone reads a phone screen in sunlight, and everyone's contrast sensitivity declines with age.

The most common real-world failure is light grey text on white. #999999 on white gives about 2.8:1 — comfortably below the 4.5:1 minimum, and a very popular choice because it looks elegant on a designer's calibrated monitor in a dim room. On a laptop outdoors it is close to invisible. Darkening to #767676 reaches 4.54:1 and passes, at almost no cost to the look.

The second most common is white text on a mid-saturation brand colour. A vivid blue button often lands around 3:1 with white — fine for large text, failing for anything at body size.

Never let colour carry the meaning alone

Around 8% of men and 0.5% of women have some form of colour vision deficiency. The most common types make red and green hard to distinguish — which is unfortunate, because red-for-bad and green-for-good is the most widespread convention in interface design.

The rule is not "avoid colour." It is that colour should never be the only channel carrying information:

A quick way to test: view the design in greyscale. If it still communicates everything, colour is doing decoration rather than load-bearing work. If it becomes ambiguous, something needs a second channel.

Practical defaults

Keep hex for storage, use HSL for thinking. Store and share colours as hex — it is the universal format. But when deciding what a colour should be, convert to HSL, adjust the dimension you actually mean, and convert back.

Build a scale, not a set of one-offs. Define one hue with 6–8 lightness steps and use it consistently: the lightest for backgrounds, mid values for borders and illustration, dark values for text. Most interfaces need far fewer distinct colours than they end up with.

Check contrast before you commit, not after. Retro-fitting accessibility to a finished design usually means changing colours that stakeholders have already approved. Checking the two or three critical text-on-background pairs at the start costs nothing.

Pick greys deliberately. Pure neutral greys (saturation 0) look slightly dead next to a saturated brand colour. Giving greys a small amount of the brand hue — saturation around 5–10% — makes an interface feel considered without anyone consciously noticing.

Use randomness as a starting point, not an answer. A random colour generator is genuinely useful for escaping a rut when you have no idea where to begin. But take the result as a seed: read its HSL, build a proper scale from it, and check the contrast. The generator gives you a direction; the arithmetic gives you a palette.

Tools mentioned in this guide