Colours, units and when to use each
There are about twenty ways to write a length in CSS and you need five. The same goes for colours. This lesson is about which five, and why the others exist.
Colours
color: red; /* named — 148 of them */
color: #1a4d2e; /* hex */
color: #1a4d2e80; /* hex with alpha — 80 is 50% */
color: rgb(26 77 46); /* modern space-separated syntax */
color: rgb(26 77 46 / 0.5); /* with alpha */
color: hsl(143.5 49.5% 20.2%); /* hue, saturation, lightness */
color: oklch(35% 0.07 155); /* perceptually uniform */
color: currentColor; /* whatever color is on this element */
The first three of those are the same colour, and note what it took: the hsl()
needs a decimal point in all three numbers. Converting a hex value to hsl() by
hand is lossy — round it to the tidy-looking hsl(147 50% 20%) and the browser computes
rgb(26 77 48), which is a different green. Close enough to nobody's eye, and not
the same value, which matters if you are matching a brand colour. Convert with a
tool, or pick the colour in hsl() in the first place and never write the hex.
Hex is fine and it is what most of the world uses. Use it, know the others exist, and reach for them when they help.
When each one actually helps
hsl() is worth knowing because it is the only one you can reason about. Hue
is an angle on the colour wheel, saturation is how vivid, lightness is how light.
So a set of tints is arithmetic:
--green-900: hsl(147 50% 15%);
--green-700: hsl(147 50% 25%);
--green-500: hsl(147 50% 40%);
--green-100: hsl(147 50% 92%);
Nobody can do that in hex. Changing #1a4d2e to "20% lighter" requires a tool.
oklch() fixes a real problem with hsl(): equal lightness numbers do not look
equally light. hsl(60 100% 50%) (yellow) is dramatically brighter than
hsl(240 100% 50%) (blue) despite both saying 50%. oklch() is perceptually
uniform, so 50% lightness looks the same across hues — which matters when you are
building a palette that needs consistent contrast. It is supported everywhere
current.
currentColor is the underused one:
.button {
color: #1a4d2e;
border: 2px solid currentColor; /* follows the text colour */
}
.button:hover { color: #0f3d20; } /* border follows automatically */
One declaration to change instead of two, and it is how you make an inline SVG icon take its parent's colour.
Named colours: use transparent and currentColor. Avoid the rest — red is
#ff0000, which is a harsher red than anything you would choose, and gray is
lighter than darkgray, which tells you how carefully that list was designed.
Alpha, and the one thing to know
background: rgb(0 0 0 / 0.5); /* half-transparent black background */
opacity: 0.5; /* the WHOLE element half-transparent */
These are different. opacity applies to the element and all its children —
text, borders, images. It also creates a stacking context, which is a z-index
problem for module 3. For a translucent panel with readable text, put the alpha in
the background, never on opacity.
Absolute lengths
width: 300px;
px is the only absolute unit worth using on screen. cm, mm, in, pt and
pc exist for print stylesheets; pt in particular shows up in copied CSS and
should be px.
A CSS pixel is not a device pixel. A modern phone has 2 or 3 device pixels per CSS pixel, which is why an image needs to be about twice its display size to look sharp — module 4 covers that.
Relative lengths, which are the important ones
font-size: 1.5rem; /* 1.5 × the ROOT font size */
padding: 1.5em; /* 1.5 × THIS element's font size */
width: 50%; /* of the parent's width */
height: 50vh; /* of the viewport height */
width: 50vw; /* of the viewport width */
font-size: 2ch; /* width of a "0" in this font */
rem, and why it matters for accessibility
rem is relative to the root element's font size, which is 16px unless the
visitor changed it. So 1rem = 16px, 1.5rem = 24px, 0.875rem = 14px.
The reason to use it for text is not convenience. A visitor who has set a larger
default font size in their browser gets larger text, because everything scales
from that root. Sizes in px ignore their setting entirely.
That is a real accessibility requirement, and it is a one-character decision.
body { font-size: 1rem; }
h1 { font-size: 2.5rem; }
.small { font-size: 0.875rem; }
Do not "fix" the root to make the maths easier. You will see this advice:
html { font-size: 62.5%; } /* so 1rem = 10px */
It makes 1.6rem mean 16px, which is tidier arithmetic and overrides the visitor's
preference proportionally — someone who set 20px now effectively gets 12.5px as
their base. Just learn that 1.25rem is 20px.
em, and where it is right
em is relative to the element's own font size, which makes it compound:
.card { font-size: 1.25rem; }
.card p { font-size: 0.9em; } /* 0.9 × 1.25rem, not 0.9 × root */
Nested em font sizes multiply and get away from you. But em is exactly right
for padding and margin that should scale with the text:
.button {
font-size: 1rem;
padding: 0.75em 1.5em; /* a large button gets proportionally larger padding */
}
Change the button's font-size and the padding follows. In rem it would not.
The rule of thumb: rem for font sizes, em for spacing that belongs to the
text, rem for layout spacing, px for borders and anything that should never
scale.
Percentages, and what they are a percentage of
The trap: % refers to different things depending on the property.
width: 50%— of the parent's width.padding: 5%— of the parent's width, evenpadding-top. This is whypadding-bottom: 56.25%was the old aspect-ratio hack.height: 50%— of the parent's height, and only if the parent has a height. Otherwise it is ignored, which is whyheight: 100%so often does nothing.font-size: 120%— of the parent's font size.line-height: 150%— of the element's own font size.
height: 100% doing nothing is one of the most common beginner frustrations, and
the cause is a parent with no explicit height. min-height: 100dvh on a section is
usually what you wanted.
Viewport units
min-height: 100dvh; /* dynamic — accounts for the phone address bar */
width: 100vw; /* careful: includes the scrollbar */
vh, vw, vmin, vmax, and the newer dvh/svh/lvh. Use dvh for full
height. Avoid 100vw for widths — it includes the vertical scrollbar's width, so on
a page that scrolls it is wider than the content area and gives you a horizontal
scrollbar. 100% does not have this problem.
Do not size text in vw alone — it stops responding to the visitor's font
preference and becomes unreadably small on a narrow screen. clamp() with a rem
component is the answer, in module 4.
calc(), min(), max(), clamp()
width: calc(100% - 2rem);
width: min(100%, 60ch); /* whichever is smaller */
padding: max(1rem, 3vw); /* whichever is larger — a floor */
font-size: clamp(1rem, 2.5vw, 2rem); /* min, preferred, max */
calc() mixes units, which is its whole point — calc(100% - 300px) for a
content area beside a fixed sidebar. Spaces around + and - are required:
calc(100%-2rem) is invalid and silently does nothing, which is a five-minute bug.
min() and max() read backwards until you get used to them. min(100%, 60ch)
means "60 characters wide, but never more than the container" — so it behaves like a
max-width. max(1rem, 3vw) is a padding that never goes below 1rem.
clamp() is three arguments — minimum, preferred, maximum — and it is one of the
most useful things in modern CSS. Module 4 uses it properly for fluid typography.
Time and angles
transition: all 200ms ease;
transform: rotate(45deg);
ms or s for time — 200ms and 0.2s are the same, and 200ms reads better.
deg, rad, turn for angles. A unitless 0 is allowed for lengths; a unitless
time is not, so transition: all 0 ease is invalid where width: 0 is fine.
Check your work
Which colour notation to default to. Hex.
What hsl() gives you. Arithmetic — a lighter shade is a bigger lightness
number.
What oklch() fixes. hsl() lightness is not perceptually even across hues.
What currentColor is for. A border or an SVG icon that follows the text
colour.
opacity versus alpha in a background. opacity fades the element and all its
children, and creates a stacking context.
The only absolute unit worth using on screen. px.
Why rem for font sizes. It respects the visitor's default font size setting.
Why not html { font-size: 62.5% }. It scales down whatever the visitor chose.
Where em is right. Padding that should scale with the element's own text.
What padding: 5% is a percentage of. The parent's width, even vertically.
Why height: 100% often does nothing. The parent has no explicit height.
Why avoid 100vw. It includes the scrollbar width and causes horizontal
scroll.
What min(100%, 60ch) behaves like. A max-width.
The calc() gotcha. Spaces around + and - are required.
Which unit cannot be omitted on zero. Time — 0 is invalid for a transition
duration.
Practice
- Write
#1a4d2eandhsl(147 50% 20%)side by side and compare the computedrgb()of each. Then tryhsl(143.5 49.5% 20.2%). Note how precise you have to be to match a hex value exactly. - Build a four-step palette from one
hsl()hue by changing lightness only. - Compare
hsl(60 100% 50%)andhsl(240 100% 50%)side by side. Then the same two inoklch()at equal lightness. - Use
currentColorfor a button's border and change onlycoloron hover. - Put white text on a panel with
opacity: 0.5, then withbackground: rgb(0 0 0 / 0.5). - Set
html { font-size: 24px }and see which of your sizes follow. Then change your browser's default font size instead. - Add
html { font-size: 62.5% }and work out what a visitor who chose 20px now gets. - Give a button
paddinginem, then double itsfont-size. Then repeat withrem. - Set
padding-top: 10%on a box and measure it against the parent's width and height. - Try
height: 100%on adivinside a parent with no height. Then give the parent one. - Set a section to
100vwon a page that scrolls vertically. Measure the overflow. - Write
calc(100%-2rem)and thencalc(100% - 2rem). Find the difference in devtools. - Use
min(100%, 60ch)for a text column and resize the window. - Write
transition: all 0 easeand check whether devtools accepted it.
Official documentation
- MDN — CSS values and units — Every unit, and what each percentage is a percentage of.
- MDN — color value — All the colour notations, including
oklch()and the newer colour spaces. - MDN — calc() — Including the whitespace rule that silently invalidates the whole declaration.
Next: backgrounds, borders and shadows — the decorative half of CSS.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship