WasiLearn Code
CSS · 2. Colour, text and units

CSS units: px, em, rem, % and ch

You set your nested menu to font-size: 0.9em because one level looked a touch large. Level two is smaller than you wanted. Level three is nearly illegible. Nothing is broken, no rule is being overridden, and the numbers are all 0.9.

The unit is the problem. A number in CSS is meaningless on its own — 0.9 of what is the entire question, and each unit answers it differently.

.price {
  font-size: 20px;      /* twenty pixels, always */
  padding: 1em;         /* twenty pixels, because the font is 20px */
}

Two declarations, same rendered value, completely different promises.

px is the fixed one

px does not move. border: 1px is one pixel at every font size, in every container, on every page.

One clarification, since it confuses people: a CSS pixel is not necessarily a hardware pixel. On a high-density phone screen the browser may paint one CSS pixel across four physical ones, which is why a 16px font is not microscopic on a modern phone. You do not manage this; the browser does.

Use px where a value genuinely should not scale: hairline borders, small fixed offsets, the odd icon that must land on an exact size.

Avoid it for body text. A reader who has raised their browser's default font size — because they need to — gets no benefit from a page hard-coded to 14px.

% is relative to the parent, but to which measurement?

A percentage is always a share of something in the parent, and the something changes by property. This catches people out:

  • width: 50% — half the containing block's width.
  • font-size: 120% — 1.2× the parent's font size.
  • line-height: 150% — 1.5× this element's own font size.
  • padding: 10% and margin: 10% — a share of the containing block's width, on all four sides.

That last one is the surprise. padding-top: 10% measures against width, not height. It looks like a bug and it is deliberate — it is how people used to force a fixed aspect ratio before aspect-ratio existed.

em measures from the element's own font size

em is relative to the font size of the element the declaration is on.

.btn {
  font-size: 14px;
  padding: 1em;      /* 14px */
}

.btn-large {
  font-size: 20px;
  padding: 1em;      /* 20px */
}

One padding declaration, two proportional results. The large button's breathing room grows with its text automatically, so it looks like the same button rather than a small button with a big label. This is em doing exactly what it is best at: spacing that belongs to the type.

There is one special case, and it is the one that causes all the trouble. When the property is font-size, the element's own font size is what you are busy calculating, so em falls back to the parent's computed font size instead.

The compounding trap

Beginners treat em and rem as spellings of one idea, and describe em as "relative to the parent". Half right, and the wrong half hurts.

Here is a menu with three levels of nesting:

<ul class="menu">
  <li>Routes
    <ul>
      <li>Karachi
        <ul>
          <li>Saddar</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>
.menu ul {
  font-size: 0.9em;
}

If em really meant "relative to the parent's original size", every level would be 0.9 of 16px — 14.4px — and it would stop there. That is what most people expect, and the first level agrees with them, which is what makes the belief so convincing.

But the parent's font size is not 16px any more. It is whatever the previous 0.9em produced. So the multiplications stack:

Level Calculation Computed
.menu inherited root 16px
level 2 16 × 0.9 14.4px
level 3 14.4 × 0.9 12.96px
level 4 12.96 × 0.9 11.664px

That is 0.9 × 0.9 × 0.9 = 0.729 of where you started. Add two more levels and the text is under 10px. Every step is obeying your rule perfectly; the rule just compounds.

rem is the fix. It is relative to the root font size — the <html> element, 16px unless the user changed it — no matter how deep the element sits:

.menu ul {
  font-size: 1rem;    /* 16px at every level, forever */
}

Nothing compounds, because nothing is measured against the level above.

So the working split is: rem for anything that must not compound — type scale, section spacing, component sizes — and em for anything that should deliberately track the local font size, which in practice means padding, margin-bottom on a heading, and gaps inside a button.

One more warning while you are here. You will see advice to set html { font-size: 62.5% } so that 1rem becomes a convenient 10px. It works, but it shrinks the size the reader chose in their browser settings by nearly 40%. Leave the root alone and get comfortable with 16.

ch measures in characters

1ch is the width of the 0 glyph in the element's current font.

Its use is line length. Long lines are hard to read because your eye loses the next line on the return sweep; the comfortable range is roughly 45 to 75 characters.

article p {
  max-width: 65ch;
}

That is a much better instruction than max-width: 700px, because it means what you actually want. Change the font size, and the measure keeps holding about the same number of characters.

Be honest about the approximation, though. ch measures the digit zero, and in most fonts letters average narrower, so 65ch typically holds rather more than 65 characters. It also shifts when you change font family — a condensed face fits far more into the same ch count. It is a good approximation, not a guarantee.

The same paragraph, three ways

Put one paragraph inside a 400px card and again inside a 900px card:

.p-a { width: 25%; }
.p-b { width: 250px; }
.p-c { width: 60ch; }
  • 25% is 100px in the small card and 225px in the large one. It promises a quarter of whatever contains me.
  • 250px is 250px in both — which overflows the 400px card once padding is added. It promises this exact size, container be damned.
  • 60ch is the same reading measure in both, until the container is narrower than that, at which point it overflows too. It promises this many characters per line.

None is right in general. Pick the one whose promise matches the sentence you would say out loud about the design.

Viewport units, briefly

1vw is one per cent of the viewport width, 1vh one per cent of its height. 100vh is a full screen.

On phones there is a wrinkle: the address bar slides away as you scroll, so "the screen" has two heights. 100vh uses the larger one, which means a supposedly full-height section can be taller than the visible area at first. 100dvh — dynamic viewport height — tracks the space actually visible right now, and is usually what you meant.

A default set worth starting from

If you want one line to remember: rem for type and vertical rhythm, em for padding inside a component, px for borders, % and ch for widths, and dvh when something must fill the screen.

Next: calc(), which lets you mix these in a single value — a width of 100% minus a fixed sidebar — and comes with a whitespace rule that fails completely silently. The colour lesson covers the other half of the values you will be writing today.

Prefer learning with a teacher?

WasiLearn Academy runs small-group classes covering this material, online and in Karachi.

See the classes