CSS from the ground up

Colour and units

Writing a colour three ways, measuring in units that scale, and the contrast rule that keeps text readable.

Part 3 of 12

Beginner14 min readBack to Courses

A colour is three numbers

Every colour you see is red, green and blue mixed in some amount. #059669 is hexadecimal — two digits each for red, green and blue — and rgb(5, 150, 105) is the same colour written in plain numbers. hsl(160, 94%, 30%) says it differently again: a hue, how saturated it is, how light.

The formats are interchangeable, so choose by what you are doing. Hex is compact and everywhere; rgb() makes an alpha channel easy — rgb(5 150 105 / 0.5) is the same green at half transparency; hsl() is the one to reach for when you want a lighter or darker version of a colour by changing one number.

One colour, three ways

.emerald { color: #059669; }
.emerald-soft { background: rgb(5 150 105 / 0.12); }
.emerald-dark { color: hsl(160 94% 22%); }

Units that scale

px is a pixel — predictable, and blind to the reader’s settings. rem is the size of the page’s base text: 1.5rem is one and a half times the reader’s own font size, so it grows when they enlarge text and shrinks on a small screen. % and vw measure against a parent and the viewport.

The habit that keeps pages readable: use rem for type and spacing, % or ch for widths, and reach for px for things that should not scale, like a hairline border.

The rule behind the colour

Text has to be readable: 4.5 to 1 between the text colour and its background, at the sizes this page uses. Pale grey on white looks elegant in a mockup and vanishes on a phone in sunlight.

Contrast is measurable, and browsers will show you the ratio in their colour pickers. If a colour pair is close, darken the text rather than lightening the design — the person struggling to read it is not the one who will complain.

Contrast is a property of the pair, not of the colour. The same grey is readable on white and invisible on a mid-tone surface, so the ratio has to be checked against the background the text actually sits on — including a background image, where the safe answer is a solid layer behind the text. Large text is allowed a lower ratio, around 3 to 1, because the letterforms are bigger; small text gets no such allowance.

Reading a colour backwards

A hex value is six base-sixteen digits in three pairs: red, green, blue, each from 00 to ff. #059669 is 05 red, 96 green and 69 blue — very little red, a lot of green, a medium blue. The browser converts all three formats to the same internal numbers, which is why they can be swapped without a visible change.

HSL describes that same colour with different coordinates. Hue is an angle on the colour wheel, from 0 for red through 120 for green to 240 for blue; saturation is how far the colour is from grey; lightness runs from black at 0% to white at 100%. At 30% lightness the green is deep enough to read as text on white.

Which unit resolves against what

A relative unit only means something next to the thing it resolves against. rem looks at the root font size; em looks at the font size of the element itself, so nested em values multiply. A width in % looks at the parent’s content width, and vw looks at the viewport — including the strip a scrollbar occupies in some browsers.

That difference is the whole decision. rem keeps spacing and type tied to the reader’s setting. em is right when a component should scale as a unit, such as the padding inside a button whose label grows. vw is right for a full-bleed band and wrong for body text, where a narrow window can shrink the words below reading size.

Three colour and unit traps: % padding resolves against the element’s width even when it is padding-top, so vertical percentages surprise you; nested em sizes compound, so a 0.9em heading inside a 0.9em section is smaller than either rule suggests; and 100vw includes the scrollbar, so a full-width band can cause sideways scrolling. When a value is not the size you expected, ask what it is measuring against.

Try these next

  • Build three steps of one hue with hsl(): a pale surface, a mid-tone border and a dark text colour.
  • Set the body text in rem and a hairline rule in px, then raise the browser’s default font size and compare.
  • Write one padding-top in % and one in rem, and explain why only the first changes when the window narrows.

A small palette, written three ways

Give the heading a hex colour, the notice a see-through rgb background, and the page’s padding in rem. Three declarations, three formats.

Live Preview
Console output will appear here...

3 left to do

  • A hex colour is used
  • An rgb() colour is used
  • A rem measurement is used

color: #059669;, background: rgb(5 150 105 / 0.12);, padding: 2rem;.

One hue, three jobs

Build a card from a single hue: a pale surface, a mid-tone border and dark text, all written with `hsl()`. Measure the space inside in `rem`, and keep the edge at one pixel.

Live Preview
Console output will appear here...

3 left to do

  • The card colours are written with hsl()
  • The card has a one-pixel edge
  • The inside space is measured in rem

Keep the hue and saturation fixed and change only the lightness: hsl(160 40% 95%) for the surface, hsl(160 40% 70%) for the border and hsl(160 40% 20%) for the ink.

hsl() is the friendliest format for building a palette: keep the hue and saturation, change only the lightness, and every shade belongs to the same family.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Which two values describe the same colour?