
Understanding CSS Units: px, em, rem, vw, vh, and More
Every length in CSS comes with a unit, and the one you pick decides how that length behaves when things change. Will this padding grow when the user bumps up their browser's font size? Will this heading shrink on a phone? Will this sidebar stay the same width inside a narrow container? The answers depend almost entirely on units.
CSS has a lot of them, from the familiar px to newer arrivals like cqi and rlh. The good news is that they fall into a few families, and once you understand what each family is relative to, choosing the right unit becomes much less of a guessing game. This guide walks through each family, explains the trade-offs, and ends with a practical cheat sheet you can apply to real projects.
Absolute vs. Relative Units
The first split is between units with a fixed size and units that depend on something else.
- Absolute units have a fixed relationship to each other:
px,cm,mm,in,pt,pc, andQ. - Relative units are calculated from something else: the font size (
em,rem,ch), the viewport (vw,vh), a container (cqw,cqi), or the parent element (%).
In practice, almost everything interesting happens with relative units. Absolute units other than px are mainly useful for print stylesheets.
Pixels (px)
The CSS pixel is the unit most developers learn first. Despite the name, a CSS pixel isn't a physical pixel on your screen. It's a "reference pixel" defined so that content looks roughly the same size across devices. On a high-density phone display, one CSS pixel might cover two or three physical pixels.
All the other absolute units are defined in terms of it: 1in is always 96px, 1cm is about 37.8px, and 1pt is 1.333px. That's why 1cm on a monitor rarely measures one centimeter with a ruler.
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 2px 6px rgb(0 0 0 / 0.08);
}
When to use px : Borders, hairlines, shadows, and small decorative details that shouldn't scale with text. A 1px border that became 1.5px because the user increased their font size would look blurry and odd.
When to avoid it : Font sizes, and usually spacing around text. Browsers let users set a default font size for readability. If you set font-size: 16px on the root, you override that preference. Browser zoom still works with pixels, but the default font-size setting doesn't, and some users rely on it.
Font-Relative Units
em
An em equals the computed font size of the current element. When used on the font-size property itself, it refers to the parent's font size.
.button {
font-size: 1rem;
padding: 0.6em 1.2em;
border-radius: 0.4em;
}
.button--large {
font-size: 1.25rem;
}
This is where em shines. The padding and corner radius are relative to the button's own font size, so the large variant scales all of its internals proportionally without redefining them. One change to font-size resizes the whole component.
The downside is compounding. If you use em for font sizes on nested elements, each level multiplies the one above it:
<ul class="menu">
<li>
Level one
<ul class="menu">
<li>
Level two
<ul class="menu">
<li>Level three</li>
</ul>
</li>
</ul>
</li>
</ul>
.menu {
font-size: 0.9em;
}
The first level is 90% of the body text, the second is 81%, the third is 72.9%, and so on. Sometimes that's intentional, but usually it's a bug waiting to happen.
rem
A rem ("root em") equals the font size of the root element, html. It doesn't compound, because it always refers to the same value no matter how deeply nested the element is.
html {
font-size: 100%;
}
body {
font-size: 1rem;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 1.75rem;
}
.section {
padding-block: 4rem;
}
With html left at 100% (the browser default, usually 16px), 1rem tracks the user's preferred font size. If they've set their browser default to 20px, every rem-based size scales up by 25%, and your layout scales with it.
A note on the 62.5% trick : You may see html { font-size: 62.5%; } so that 1rem equals 10px, making math easier. It respects user preferences, since it's a percentage, but it forces you to reset the body font size and makes third-party components that assume a 16px root render tiny. Modern tooling and calc() make this trick mostly unnecessary.
em or rem?
A simple, reliable rule:
- Use
remfor font sizes and for layout-level spacing, such as section padding, gaps between components, and max widths for text. - Use
emfor spacing that should scale with a component's own text, such as button padding, icon sizes next to text, and underline offsets.
ch and ex
1chis the width of the "0" character in the current font.1exis roughly the height of a lowercase "x".
ch is perfect for limiting line length, which has a big effect on readability:
.prose {
max-width: 65ch;
}
Around 45 to 75 characters per line is the commonly cited comfortable range. Because ch is based on a single character's width, 65ch isn't exactly 65 characters of mixed text, but it's close enough, and it scales with the font.
ch is also handy for inputs whose content has a known length:
.input-zip {
width: 7ch;
}
.input-year {
width: 6ch;
}
lh and rlh
The lh unit equals the element's computed line height, and rlh equals the root element's line height. They're useful for vertical rhythm, sizing things in multiples of lines of text:
.excerpt {
max-height: 3lh;
overflow: hidden;
}
.stack > * + * {
margin-top: 1rlh;
}
The first rule limits an excerpt to exactly three lines of text, whatever the font size or line height. Both lh and rlh are supported in current versions of the major browsers, but they're recent additions, so provide a fallback if you support older ones:
.excerpt {
max-height: 4.5em;
max-height: 3lh;
}
Browsers that don't recognize lh keep the first declaration.
Percentages (%)
Percentages are relative to something on the parent, but what exactly depends on the property:
width,padding, andmargin: relative to the width of the containing block. Yes, evenpadding-topandmargin-topuse the width, which surprises almost everyone the first time.height: relative to the containing block's height, but only if that height is explicitly defined. Otherwise, a percentage height typically behaves likeauto.font-size: relative to the parent's font size, just likeem.transform: translate(): relative to the element's own size.line-height: relative to the element's own font size.
.sidebar {
width: 30%;
}
.modal {
translate: -50% -50%;
}
That last example is the classic centering trick: move the element left and up by half of its own width and height.
Viewport Units
Viewport units are relative to the size of the browser's viewport:
1vw= 1% of the viewport width.1vh= 1% of the viewport height.1vmin= 1% of the smaller dimension.1vmax= 1% of the larger dimension.
.hero {
min-height: 80vh;
}
.full-bleed {
width: 100vw;
margin-inline: calc(50% - 50vw);
}
Viewport units have two well-known gotchas:
100vwincludes the scrollbar. On platforms with classic scrollbars,width: 100vwis wider than the visible page and causes horizontal scrolling. For full-width sections,width: 100%on a block element is usually what you want.100vhon mobile is taller than the visible area. Mobile browsers show and hide their address bar as you scroll, andvhis based on the largest possible viewport. A100vhhero can push its bottom content off screen.
The second problem led to a new set of units: svh, lvh, and dvh (small, large, and dynamic viewport height), plus matching width, inline, and block variants. They deserve a full discussion of their own, but the short version is that dvh tracks the visible viewport as the browser UI appears and disappears, and svh gives you the smallest, most conservative size.
.hero {
min-height: 100vh;
min-height: 100svh;
}
The fallback line keeps older browsers working.
Logical viewport units
vi and vb are the logical versions of vw and vh: 1% of the viewport's size in the inline direction and the block direction. In a horizontal writing mode like English, vi equals vw and vb equals vh, but they adapt correctly to vertical writing modes.
Container Query Units
Viewport units look at the whole browser window, which is often the wrong reference for a reusable component. A card in a narrow sidebar and the same card in a wide main column experience the same viewport. Container query units fix this by measuring the nearest ancestor that's been declared a container:
cqw/cqh: 1% of the container's width / height.cqi/cqb: 1% of the container's inline size / block size.cqmin/cqmax: 1% of the smaller / larger dimension.
.card-grid > * {
container-type: inline-size;
}
.card__title {
font-size: clamp(1rem, 4cqi + 0.5rem, 1.75rem);
}
.card__media {
aspect-ratio: 16 / 9;
border-radius: 2cqi;
}
Now the title scales with the card's width, not the window's. Container query units are supported in all current major browsers. If there's no container ancestor, they fall back to the small viewport units, so nothing breaks outright.
The Fraction Unit (fr)
fr only works in CSS Grid track sizing. It represents a share of the leftover space after fixed-size tracks and gaps are accounted for.
.layout {
display: grid;
grid-template-columns: 16rem 1fr 2fr;
gap: 1.5rem;
}
Here the first column is 16rem, and the remaining space is split into three parts: one for the middle column and two for the last. Unlike percentages, fr automatically accounts for gaps, so you never end up with a grid that's slightly wider than its container.
One subtlety: 1fr has an implicit minimum of auto, meaning a track won't shrink below its content's minimum size. If long words or wide images make a column overflow, use minmax(0, 1fr) instead.
Unitless Values
Some properties accept numbers with no unit, and for line-height that's the recommended form:
body {
line-height: 1.6;
}
A unitless line height is inherited as a ratio, so each child recalculates it against its own font size. If you write line-height: 1.6em instead, the computed pixel value is inherited, and a large heading inside the body gets the body's small line height, causing overlapping lines.
Other unitless properties include z-index, opacity, flex-grow, flex-shrink, order, and aspect-ratio. And 0 never needs a unit for lengths: margin: 0 is the same as margin: 0px.
Other Unit Types
Not every unit is a length. You'll run into these too:
- Angles :
deg,rad,grad, andturn.rotate: 0.25turnis the same asrotate: 90deg. - Time :
sandms, for transitions and animations. - Resolution :
dppxanddpi, mostly in media queries like(resolution >= 2dppx).
Mixing Units with calc(), min(), max(), and clamp()
The real power comes from combining units. Math functions let you mix any compatible units in one expression:
.container {
width: min(100% - 2rem, 72rem);
margin-inline: auto;
}
h1 {
font-size: clamp(2rem, 1.25rem + 3vw, 3.5rem);
}
.sidebar {
width: max(16rem, 25%);
}
- The container is 72rem wide, but never wider than the available space minus 1rem of gutter on each side.
- The heading scales smoothly with the viewport, between 2rem and 3.5rem. Including a
remterm in the middle value is important: a purevwfont size doesn't respond to browser zoom in the same way, which can fail accessibility requirements for text resizing. - The sidebar is a quarter of the width, but never narrower than 16rem.
A Practical Cheat Sheet
| Use case | Recommended unit |
|---|---|
| Root and body font size | % or rem |
| Headings and text sizes | rem, or clamp() with rem + vw |
| Component padding tied to its text | em |
| Layout spacing and gaps | rem |
| Line length for reading | ch |
| Line height | unitless |
| Borders, hairlines, shadows | px |
| Grid columns | fr, minmax() |
| Full-height hero sections | svh or dvh with a vh fallback |
| Component-responsive sizing | cqi |
| Media query breakpoints | em or rem |
About that last row: media queries measure em and rem against the browser's default font size, not your root styles. Using em breakpoints means the layout responds correctly when users increase their default text size, switching to the narrower layout sooner, which is usually what they need.
@media (width >= 48em) {
.layout {
grid-template-columns: 16rem 1fr;
}
}
Common Pitfalls
- Setting a pixel font size on
html: It overrides the user's preferred default font size. Leave it at100%or omit it. - Compounding
emfont sizes : Nested elements that each setfont-sizeinemshrink or grow unexpectedly. Useremfor font sizes unless you want the compounding. - Using
100vwfor full-width elements : It includes the scrollbar width on some platforms and causes horizontal overflow. - Using
100vhfor mobile hero sections : Content gets hidden behind the browser toolbar. Usesvhordvh. - Percentage heights that do nothing : The parent needs an explicit height, or use a different approach such as Flexbox, Grid, or viewport units.
- Line height with units :
line-height: 24pxor1.5eminherit as fixed values. Use a unitless number.
Conclusion
CSS units aren't interchangeable; each one answers the question "relative to what?" in a different way. Pixels are fixed and precise, good for borders and details. rem ties sizes to the user's preferred text size, which makes it the backbone of accessible typography and spacing. em scales things with their own component. ch and lh measure in characters and lines. Viewport and container units respond to available space, and fr divides grid space cleanly.
Pick the unit whose reference point matches your intent, combine units with clamp() and friends when you need both flexibility and limits, and your layouts will scale gracefully across screens, containers, and user preferences.


