CSS units: px, rem, em, vh and when to use each
CSS units compared: nested em compounds to 67.5px where rem stays at 24px. That difference is the whole reason to prefer one over the other.
Every CSS length is either absolute (a fixed size) or relative (calculated from something else). The whole question is what that something else is:
| Unit | Relative to | Typical use |
|---|---|---|
px |
Nothing, it is absolute | Borders, shadows, tiny fixed details |
rem |
The root font size | Almost everything: type, spacing, widths |
em |
The element's own font size | Padding that should scale with its text |
% |
The parent's corresponding size | Fluid widths |
vh / vw |
The viewport | Full-screen sections |
ch |
The width of a 0 in the current font |
Reading-width limits |
The one that causes real bugs is em, and the reason is measurable.
em compounds, rem does not
Three nested elements, each with font-size: 1.5em, inside a parent at 20px:
html { font-size: 16px; }
.parent { font-size: 20px; }
.em-a, .em-b, .em-c { font-size: 1.5em; }
<div class="parent">
<div class="em-a">a<div class="em-b">b<div class="em-c">c</div></div></div>
</div>
Measured font sizes: 30px, 45px, 67.5px.
Each level multiplies the one above it. 20 × 1.5 = 30, then 30 × 1.5 = 45, then 45 × 1.5 = 67.5. The same declaration produces a different result at every depth.
Now the identical structure with rem:
.rem-a, .rem-b, .rem-c { font-size: 1.5rem; }
Measured: 24px, 24px, 24px.
Every level is 1.5 × the root's 16px, regardless of nesting. The declaration means the same thing everywhere it appears.
That is the entire case for rem as your default. em is not broken, it is doing exactly what it says, and "relative to the parent" is a property that compounds in a way nobody tracks in their head across a real component tree.
Where em is the right answer
em earns its place when you want the value tied to the element's own text. The clearest case is a button:
.button {
font-size: 1rem;
padding: 0.75em 1.5em;
border-radius: 0.5em;
}
The padding is defined in terms of the button's own font size, so a larger button gets proportionally larger padding for free:
.button-lg { font-size: 1.25rem; } /* padding scales with it */
One declaration resizes the whole component. Doing that with rem padding would need every value changed.
The rule worth applying: rem for font sizes, em for spacing that should scale with the font size it sits next to. That combination avoids compounding, because font-size is the only property that feeds the next level down.
em is also the right unit in media queries, where it behaves consistently across browsers in a way rem historically did not.
Why rem over px for type
px is absolute and predictable, which sounds like an advantage. The problem is accessibility.
A user who sets a larger default font size in their browser is telling every site they visit that they need bigger text. rem respects that, because the root font size is what they changed. px ignores it entirely, and text set in px stays small no matter what the user asked for.
That is the argument, and it is a strong one. It also means you should not set html { font-size: 62.5% }, a trick that circulated for years so that 1rem would equal 10px and the arithmetic would be easier. It overrides the user's preference, which is the one thing rem was meant to preserve.
If you want easier arithmetic, use a scale of values rather than resizing the root:
:root {
--text-sm: 0.875rem; /* 14px at default */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
}
px remains correct for things that should not scale with text: a 1px border, a 2px focus ring, a hairline separator. Nobody needs a thicker border because they increased their font size.
Percentages, and what they measure against
Measured, a width: 50% inside a 400px parent rendered at exactly 200px. Straightforward for width.
Percentages are less obvious elsewhere, because different properties measure against different things:
widthis a percentage of the parent's width. Expected.heightis a percentage of the parent's height, and does nothing if the parent's height isauto. This is the single most common percentage failure.paddingandmarginare percentages of the parent's width, on all four sides.padding-top: 50%is half the parent's width, which is the old trick for a fixed aspect ratio beforeaspect-ratioexisted.font-sizeis a percentage of the parent's font size, so it compounds exactly likeem.transform: translate()is a percentage of the element's own size, which is what makes the centring transform work without knowing dimensions.
That padding row surprises everyone the first time. It is not a quirk, it is how the specification avoids a circular dependency where a box's height would depend on its own height.
Viewport units
Measured in a viewport 708px tall and 976px wide, 50vh resolved to 354px and 50vw to 488px. Exactly half of each, as expected.
The complication is mobile. 100vh is measured against the largest possible viewport, meaning the one with the browser's address bar hidden. On a phone where the bar is visible, a 100vh element is taller than the visible area and its bottom is cut off. This is the reason so many mobile full-screen sections have a button you cannot reach.
The newer units solve it:
| Unit | Means |
|---|---|
svh |
Small viewport height, with browser chrome visible |
lvh |
Large viewport height, with chrome hidden. Same as vh |
dvh |
Dynamic, updates as the chrome appears and disappears |
100dvh is what most people want when they write 100vh, and it is well supported now. Use it for anything that must fit the screen exactly.
One caution on dvh: because it changes as you scroll on mobile, animating or transitioning a dvh-based height can produce visible jumps. For a full-screen hero it is right; for an element whose height matters mid-scroll, svh is steadier.
ch, and reading width
Measured, width: 10ch in a monospace font rendered at 87.97px.
1ch is the width of the 0 character in the current font, so ch sizes boxes in terms of how much text fits. Its best use is constraining line length:
.prose { max-width: 65ch; }
Typographic guidance puts comfortable reading at roughly 45 to 75 characters per line, and ch expresses that directly. A max-width in pixels has to be re-tuned whenever the font or its size changes; a max-width in ch does not.
It is approximate in a proportional font, since 0 is not the average character width. It is close enough for the job, and far closer than guessing a pixel value.
The absolute units nobody should use on screen
px is not the only absolute unit. There are also pt, pc, cm, mm, in and Q.
They are defined in terms of each other at fixed ratios: 1in is 96px, 1pt is 1/72in, and so on. That means 1cm in CSS is not a centimetre on your screen. It is 37.8 CSS pixels, which is a physical centimetre only if the display happens to be at the reference density the specification assumes.
So for screens they are px with worse arithmetic. Their real home is print stylesheets, where the output is genuinely measured in physical units:
@media print {
@page { margin: 2cm; }
body { font-size: 11pt; }
}
If you see pt in a screen stylesheet, it usually arrived from a design tool or a word processor rather than a decision.
Container query units
The newest relative units, and the most useful addition in years:
.card { container-type: inline-size; }
.card h2 { font-size: 5cqi; }
| Unit | Relative to |
|---|---|
cqw / cqh |
The container's width / height |
cqi / cqb |
The container's inline / block size |
cqmin / cqmax |
The smaller / larger of the two |
Viewport units size things against the screen. Container units size them against the parent container. That difference matters for components, because a card in a sidebar and the same card in a main column should not have the same heading size, and vw cannot tell them apart.
This is what makes genuinely reusable components possible: the component responds to the space it was given rather than to the size of the window. Combined with container queries themselves, it removes a large share of the media queries a layout used to need.
Choosing, in practice
| For | Use |
|---|---|
| Font sizes | rem |
| Spacing inside a component | em, so it scales with the component's text |
| Spacing between components | rem |
| Borders, outlines, shadows | px |
| Fluid widths | % or fr in grid |
| Reading width | ch |
| Full-screen sections | dvh, not vh |
| Media query breakpoints | em |
The short version: rem by default, em when you want it to scale with local text, px for hairlines, dvh for full screens.
calc, and mixing units
width: calc(100% - 2rem);
height: calc(100dvh - 4rem);
calc() is what lets you combine units that cannot otherwise be added, which is most of the reason it exists. The common pattern is "fill the space, minus a fixed gutter".
Two syntax rules that trip people up. Spaces around + and - are required, because -2rem would otherwise be read as a negative value rather than a subtraction. And you cannot mix a unitless number with a length except through multiplication or division.
calc() also composes with custom properties, which is where it becomes genuinely useful:
:root { --gutter: 1rem; }
.page { padding: calc(var(--gutter) * 2); }
That combination is covered further in CSS variables.
Fluid sizing with clamp
The modern way to size type and spacing without a stack of media queries:
h1 { font-size: clamp(1.75rem, 4vw + 1rem, 3rem); }
Three values: a minimum, a preferred value, and a maximum. The heading scales with the viewport between 1.75rem and 3rem and never leaves that range.
The 4vw + 1rem middle term is doing real work. A pure vw value scales linearly from zero, so it shrinks too fast on narrow screens and the text becomes unreadable before the minimum catches it. Adding a rem component gives it a floor to grow from, so the curve is gentler and the type stays proportionate at every width.
There is an accessibility requirement here too. A preferred value in vw alone cannot be scaled by the user's zoom, because the viewport grows as they zoom out. Including a rem term keeps the value partly tied to the root font size, which preserves the user's ability to resize text. That is the reason the + 1rem is not decoration.
Three common mistakes
Using em for font sizes in nested components. The compounding above is silent and only shows up when a component is used inside another one. rem removes the class of bug entirely.
Setting html { font-size: 62.5% }. It makes 1rem equal 10px and overrides the user's browser setting, which defeats the reason to use rem at all.
Using 100vh for a full-screen section. On mobile it is taller than the visible viewport and the bottom is unreachable. 100dvh is the fix.
Quick reference
font-size: 1.125rem; /* type: rem */
padding: 0.75em 1.5em; /* internal spacing: em */
margin-block: 2rem; /* between components: rem */
border: 1px solid; /* hairlines: px */
max-width: 65ch; /* reading width */
min-height: 100dvh; /* full screen, mobile-safe */
width: calc(100% - 2rem); /* fill minus a gutter */
@media (min-width: 40em) { } /* breakpoints: em */
Want to change a root font size and watch a whole page rescale? Start with the CSS track, or read how to center a div for these units doing real layout work.
More from the blog
z-index not working? It is the parent, not the number
A child with z-index 9999 loses to a sibling with z-index 1 when the parent has opacity. Same markup without it, and 9999 wins.
Read moreCSS variables in practice: scope, fallbacks and theming
Custom properties inherit and cascade, which is what separates them from Sass variables. An invalid value falls back further than you expect.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.