← back to the section

CSS has dozens of units, but the meaningful choice is between five or six. The choice affects not code aesthetics but behavior: whether the interface scales for a user with a large font, whether a component survives a move to a different context, how many media queries you'll have to write. Let's go through the working set.

px, rem, em: three frames of reference

px — an absolute unit. Predictable, but deaf to user settings: if the browser is set to a larger default font, px sizes won't respond.

rem — a fraction of the root font size (16px by default, changeable via the browser setting). 1.5rem = 24px for most people, but it will honestly grow for whoever set it larger. The rule in modern projects: font sizes — only in rem; spacing and radii — rem or px per the team's taste (rem makes the whole grid scale along with the text).

em — a fraction of the font size of the current element. Treacherous through cascading multiplication (em inside em inside em), but irreplaceable locally: a button's padding in em grows with the button's font — the component scales with a single font-size edit. The practical duo: global — rem, inside components — em, precise graphics — px.

There are content-based units too: ch — the width of a digit character, ideal for limiting text line length (max-width: 65ch), and lh — the line height.

Percentages: what matters is the base

A percentage is always computed from something on the parent, and "from what" depends on the property:

  • width: 50% — from the parent's width;
  • height: 100% — from the parent's height, but only if it's set explicitly (otherwise ignored — the famous "height: 100% doesn't work");
  • padding and margin in percentages — from the parent's width, even the vertical ones (a historical fact);
  • font-size: 120% — from the parent's font (a synonym for 1.2em);
  • transform: translateX(50%) — from the size of the element itself (which is why centering an absolute element is left: 50% + translateX(-50%): the first percentage is from the parent, the second from itself).

Half of all "broken" percentages are a wrong guess about the reference base.

Viewport units: vw, vh and their mobile heirs

1vw/1vh — a percent of the window's width/height. Full-screen sections (min-height: 100vh), elements sized "to the whole screen" regardless of parents. Plus the derivatives vmin/vmax.

The mobile pain: the browser bar hides and slides back — the "window height" changes, and 100vh either scrolls or jumps. The spec's answer is three families: svh (small viewport — bar visible), lvh (large — bar hidden), dvh (dynamic — tracks the actual state). For full-screen blocks today you write min-height: 100dvh with a 100vh fallback one line above.

clamp: a fluid value with limiters

The function that ate half of all media queries:

h1 { font-size: clamp(1.5rem, 1rem + 2.5vw, 2.5rem); }

clamp(min, preferred, max) — the preferred value depends on the window width (via vw) but is pinned by the bounds. The heading grows smoothly from phone to monitor without a single breakpoint. The same technique works for section spacing (padding-block: clamp(2rem, 5vw, 6rem)) and widths. The rem + vw mix in the middle part matters: pure vw doesn't scale with the user's font setting, and the rem addition restores that link.

Nearby — min() and max(): width: min(90%, 1200px) — "full width, but no wider than the container" in one line, without max-width.

Media queries: what's left for them

Grid auto layouts, flexbox wrapping, and clamp cover a mass of "responsiveness" on their own. Media queries remain for structural rearrangements: hiding a sidebar, turning a menu into a burger, rearranging grid areas.

.layout { display: grid; grid-template-columns: 1fr; }

@media (min-width: 768px) {
  .layout { grid-template-columns: 240px 1fr; }
}

The working approach is mobile-first: base styles for the narrow screen, min-width queries add complexity. Breakpoints are tied not to "iPhones" but to the content: where the layout breaks — that's the threshold. Beyond width, useful queries are (hover: hover) — to tell apart devices with a pointer (hover effects only there), and prefers-reduced-motion / prefers-color-scheme — respect for the user's settings. And for components that must respond to the width of their container, not the window, container queries have arrived — @container — the next step of the same idea.

In short

  • Fonts — in rem (respect for user settings), spacing inside components — em, precise graphics — px, line length — ch.
  • Percentages are computed from different bases: width — from the parent's width, height — only from an explicit height, padding/margin — always from the width, translate — from the element itself.
  • Full-screen height on mobile — 100dvh (with a 100vh fallback): plain vh jumps because of the browser bar.
  • clamp(min, rem+vw, max) — fluid typography and spacing without breakpoints; min(90%, 1200px) — a container in one line.
  • Media queries — for structural rearrangements, mobile-first, thresholds from the content; hover: hover and prefers-* are about devices and people, not pixels.
  • Grid — auto-fit/minmax: responsiveness built into the layout.
  • The box model — how units add up into a box's size.
  • Styling: gradients, shadows, and transforms — the final topic of the series.