What is the difference between em, rem, %, vw/vh, and px, and when should each be used?
Advertisement
🧩 Scenario
Architecture Walkthrough
What Each Unit Resolves Against
Every CSS unit is either absolute or relative, and the relative ones differ only in what they reference.
px is absolute in CSS terms. One CSS pixel is a fixed reference size independent of device pixels, so it does not scale with anything. It is the right choice for values that genuinely should not scale: hairline borders, small fixed offsets, and box shadow spread.
em resolves against the font size of the element itself, except in the font-size property, where it resolves against the parent's font size. That dual behaviour is the source of most em confusion. Its useful property is that it scales with the local type size, which makes it correct for spacing that should track the text: padding inside a button, the gap between an icon and its label, or a margin-bottom proportional to the line.
rem resolves against the root element's font size, which is the html element and defaults to 16px unless the user has changed their browser preference. Because every rem in the document references one value, there is no compounding, and because that value comes from the user's own setting, rem respects text-size preferences automatically. This is why it is the default unit for type and spacing scales in modern design systems.
% resolves against the corresponding dimension of the containing block, with an important exception: padding and margin percentages, including the vertical ones, resolve against the containing block's inline size, not its height. padding-top: 50% therefore produces top padding equal to half the parent's width, which is the mechanism of the classic aspect-ratio hack that aspect-ratio has replaced.
vw and vh resolve against the viewport's width and height. vmin and vmax reference the smaller and larger of the two.
The em Compounding Problem
Because font-size in em resolves against the parent's font size, nested em values multiply.
A .card with font-size: 1.2em inside a 16px body computes to 19.2px. A .title inside it, also 1.2em, computes to 1.2 × 19.2 = 23.04px. Nest a third level and it grows again. The individual declarations look modest and consistent, and the result depends entirely on how deeply the component happens to be nested, which is not something a component author can know.
rem eliminates this by referencing the root every time. 1.2rem is 19.2px regardless of nesting depth. The practical rule most design systems settle on is rem for font sizes and em for spacing that should scale relative to the local font size.
The px Accessibility Problem
Setting font-size in px produces text that ignores the user's browser font-size preference in most engines. A user who has set their default text size to 24px for a visual impairment sees no change on a px-based site, while page zoom still works but scales the entire layout rather than just text.
Using rem for type means the whole scale shifts when the user changes their preference, which is the behaviour assistive settings are designed to produce. This is the strongest single argument for rem and the reason it is worth stating as a rule rather than a preference.
Related trap: the html { font-size: 62.5% } trick, once popular so that 1rem equals 10px for easier arithmetic. It still respects proportional changes to the user's preference, but it rescales everything against a 10px base, which makes the design's relationship to the user's chosen size less predictable and interacts badly with third-party components assuming a 16px root. Prefer leaving the root alone and using a type scale in rem.
The 100vh Mobile Problem
vh refers to the largest viewport height, which on mobile means the height with browser chrome collapsed. A 100vh section therefore extends beneath the visible toolbar and address bar, producing content cut off at the bottom and often an unexpected scrollbar.
The viewport unit variants fix this. dvh tracks the dynamic viewport and changes as the chrome shows and hides, svh is the small viewport with chrome visible, and lvh is the large viewport, equivalent to the classic vh. For a full-height hero or app shell, 100dvh is the correct default, with min-height rather than height so content longer than the viewport can still extend.
Note that dvh changing during scroll can cause layout to shift as the toolbar collapses, so for cases where stability matters more than exactness, svh is the safer choice.
Choosing
Use rem for font sizes and for a spacing scale. Use em for spacing that should track the local font size, and for media query widths where it respects font preferences better than px. Use % for widths within a containing block. Use dvh/svh for full-height sections, and vw sparingly, usually inside clamp() rather than alone. Use px for borders, shadows, and other details that genuinely should not scale.
Key Code Explained
/* px: fixed, does not scale — correct for hairlines and shadows */
.card {
border: 1px solid #e2e8f0;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}
/* em COMPOUNDS in font-size, because it resolves against the PARENT */
body { font-size: 16px; }
.card { font-size: 1.2em; } /* 16 x 1.2 = 19.2px */
.title { font-size: 1.2em; } /* 19.2 x 1.2 = 23.04px */
/* Same declarations, different results, purely from nesting depth. */
/* rem always references the ROOT — no compounding */
.card-rem { font-size: 1.2rem; } /* 19.2px */
.title-rem { font-size: 1.5rem; } /* 24px, whatever the nesting */
/* em is RIGHT for spacing that should track the local type size */
.button {
font-size: 1rem;
padding: 0.75em 1.5em; /* scales automatically with any font-size change */
gap: 0.5em;
}
.button--large {
font-size: 1.25rem; /* padding scales with it, no extra declarations */
}
/* Percentage padding resolves against the containing block's INLINE size */
.legacy-aspect-ratio {
width: 100%;
padding-top: 56.25%; /* 9/16 of the WIDTH, not the height */
}
.modern-aspect-ratio {
aspect-ratio: 16 / 9; /* what you should write today */
}
/* 100vh OVERFLOWS on mobile: vh is the LARGE viewport */
.hero-broken {
height: 100vh;
}
/* dvh tracks the dynamic viewport; min-height lets content grow */
.hero {
min-height: 100dvh;
}
/* svh is the small viewport — stable, no shift when chrome collapses */
.hero-stable {
min-height: 100svh;
}
/* em in media queries respects the user's font-size preference */
@media (min-width: 48em) { /* 768px at a 16px root, scales if the user changes it */
.layout { grid-template-columns: 240px 1fr; }
}
/* vw is best used inside clamp() rather than alone */
.fluid-heading {
font-size: clamp(1.5rem, 4vw, 3rem);
}
The em compounding block is the example to be able to compute on the spot. Both declarations say 1.2em and both look reasonable in isolation, yet the title ends up at 23.04px through nothing but nesting. That is the concrete case for rem in type scales: a component author cannot predict how deeply their component will be placed.
The .button and .button--large pair shows the other side of the argument. Using em for padding means a single font-size change produces a correctly proportioned larger button with no other declarations, which is exactly the behaviour you want inside a component and exactly the behaviour rem would prevent.
Tradeoffs
| Unit | Resolves against | Compounds | Respects user font size | Best for |
|---|---|---|---|---|
px | Nothing; fixed | No | No | Borders, shadows, hairlines |
em | Own font size; parent's for font-size | Yes | Indirectly | Spacing that tracks local type, media queries |
rem | Root font size | No | Yes | Type scale, spacing scale |
% | Containing block; inline size for padding/margin | No | No | Widths, fluid columns |
vw / vh | Viewport, large variant | No | No | Use inside clamp(); vh is unsafe on mobile |
dvh / svh / lvh | Dynamic / small / large viewport | No | No | Full-height sections |
What Interviewers Actually Check
- Whether you can say what each unit resolves against, including the
emexception infont-size - Whether you can work through an
emcompounding example numerically - Whether you connect
pxfont sizes to a real accessibility failure rather than calling it a style preference - Whether you know percentage padding resolves against inline size
- Whether you know why
100vhbreaks on mobile and can name the replacement
Follow-Up Questions
- Why does
embehave differently infont-sizethan in every other property? - What are
chandexunits, and where ischthe most appropriate choice? - How do
vminandvmaxbehave differently on a landscape tablet versus a portrait phone? - Why can
dvhcause layout shift during scroll, and when wouldsvhbe preferable despite being smaller? - How does
container-type: inline-sizeunlockcqwandcqiunits, and how do they compare tovw?
Common Candidate Mistakes
- Setting font sizes in
pxand breaking browser text-size scaling for users who rely on it, then treating page zoom as an equivalent substitute - Nesting
emfont sizes across component levels, producing compounding growth that depends on where the component happens to sit in the tree - Writing
height: 100vhfor full-screen sections, which overflows on mobile becausevhis the large viewport height measured with browser chrome collapsed - Assuming
padding-top: 10%resolves against the container's height, when all padding and margin percentages resolve against inline size - Applying
html { font-size: 62.5% }for convenient arithmetic, which rebases the whole design against a 10px root and interacts badly with third-party components expecting 16px
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you state what each unit resolves against, including the
emexception infont-size? - Can you work through the
emcompounding problem numerically for two nesting levels? - Can you explain why
remis the accessible default for type? - Can you explain why
100vhoverflows on mobile and name the unit that fixes it? - Can you pick the right unit for type, internal spacing, borders, and full-height sections?
Summary
CSS units differ only in what they reference. px is fixed and scales with nothing, which suits borders, shadows, and other details that should stay constant. em resolves against the element's own font size, except in font-size where it resolves against the parent's, and that exception is why nested em font sizes compound: 1.2em inside 1.2em inside a 16px body yields 23.04px. rem always references the root font size, so it never compounds and, because the root reflects the user's browser preference, it respects text-size accessibility settings that px ignores.
Percentages resolve against the containing block's corresponding dimension, with the notable exception that padding and margin percentages, including vertical ones, resolve against inline size. That is why padding-top: 56.25% once served as an aspect-ratio hack, a job aspect-ratio now does directly. Viewport units reference the viewport, and the plain vh refers to the large viewport measured with mobile browser chrome collapsed, which is why 100vh overflows on phones. dvh tracks the dynamic viewport, svh the small one, and min-height: 100dvh or 100svh is the correct form for a full-height section.
The working rules are straightforward: rem for type and spacing scales, em for spacing that should track the local font size and for media query widths, % for widths within a container, dvh or svh for full-height regions, vw inside clamp() rather than alone, and px for the details that genuinely must not scale.
Why is px bad for font sizes?
A px font size does not scale with the user browser font-size preference in most engines, so users who increase their default text size see no change. rem respects that preference.
What is the difference between vh and dvh?
vh uses the largest viewport height, which on mobile ignores the collapsing browser chrome, so 100vh overflows. dvh tracks the dynamic viewport and is the correct choice for full-height layouts.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement