RizTech Academy logo
RizTech Academy
CSS FoundationsLesson 6 of 825 min

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, even padding-top. This is why padding-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 why height: 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

  1. Write #1a4d2e and hsl(147 50% 20%) side by side and compare the computed rgb() of each. Then try hsl(143.5 49.5% 20.2%). Note how precise you have to be to match a hex value exactly.
  2. Build a four-step palette from one hsl() hue by changing lightness only.
  3. Compare hsl(60 100% 50%) and hsl(240 100% 50%) side by side. Then the same two in oklch() at equal lightness.
  4. Use currentColor for a button's border and change only color on hover.
  5. Put white text on a panel with opacity: 0.5, then with background: rgb(0 0 0 / 0.5).
  6. Set html { font-size: 24px } and see which of your sizes follow. Then change your browser's default font size instead.
  7. Add html { font-size: 62.5% } and work out what a visitor who chose 20px now gets.
  8. Give a button padding in em, then double its font-size. Then repeat with rem.
  9. Set padding-top: 10% on a box and measure it against the parent's width and height.
  10. Try height: 100% on a div inside a parent with no height. Then give the parent one.
  11. Set a section to 100vw on a page that scrolls vertically. Measure the overflow.
  12. Write calc(100%-2rem) and then calc(100% - 2rem). Find the difference in devtools.
  13. Use min(100%, 60ch) for a text column and resize the window.
  14. Write transition: all 0 ease and check whether devtools accepted it.

Official documentation

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