Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. CSS units: px, rem, em, vh and when to use each
Web Development

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.

By Max Arthur
Co-Founder & Content Marketer·September 15, 2026·10 Min read
White text reading 'CSS units explained px, rem, em and vh' on a dark purple dotted background

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:

  • width is a percentage of the parent's width. Expected.
  • height is a percentage of the parent's height, and does nothing if the parent's height is auto. This is the single most common percentage failure.
  • padding and margin are 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 before aspect-ratio existed.
  • font-size is a percentage of the parent's font size, so it compounds exactly like em.
  • 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.

Keep reading

More from the blog

White text reading 'Why your z-index is not working' on a dark purple dotted background
September 17, 2026·7 min readWeb Development

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 more
White text reading 'CSS variables explained' on a dark purple dotted background
September 16, 2026·10 min readWeb Development

CSS 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 more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons