Skip to content

Sizing and units

docs.scrimba.com

You set a font size in pixels, ship the page, and a reader who has bumped their browser text up for comfort sees no change at all. You give a card a fixed width and it overflows on a phone. Both come down to the same question: when you type a number in CSS, what is that number measured against? Some units are fixed, some scale to a font, some scale to the parent, and some scale to the screen. Once you know which is which, you stop guessing and start picking the unit that does the job.

Absolute vs relative units

CSS units split into two families. An absolute unit is a fixed size that never changes: px (a pixel) is the one you will meet everywhere. A relative unit is a size that scales to something else, like the reader's font size or the width of the screen.

Think of measuring a room. Centimetres are absolute: a wall is 400cm whether you are tall or short. "Three of my paces" is relative: it depends on whose feet are walking. Absolute units give you exact control; relative units let the page adapt to the person looking at it.

That adapting matters. Some readers set a larger default text size because it is easier on their eyes. If you size your text in a relative unit, their setting carries through and the page grows to suit them. If you lock it in pixels, it stays put and ignores them.

Every length in CSS is either absolute or relative, and the difference decides whether your layout can adapt. An absolute unit resolves to a fixed size regardless of context: px is the only absolute unit you will use in practice, since cm, pt, and the rest are aimed at print. A relative unit resolves against a reference, and which reference depends on the unit: rem and em scale to a font size, % scales to the parent, vw and vh scale to the viewport.

The reason to prefer relative units for most sizing is accessibility and responsiveness. When a reader raises their browser's default font size, anything sized in rem grows with it, while anything pinned in px stays fixed and can break the layout around it. The same holds across screen sizes: relative units let one stylesheet fit a phone and a monitor, which is the groundwork for responsive design.

The unit you choose is a decision about what your number is anchored to, and that anchor is what determines whether the layout responds to the reader or ignores them. An absolute unit resolves to the same computed length in every context. In screen CSS that is effectively only px: the physical units (cm, mm, in, pt, pc) are defined relative to the pixel anyway (1in is 96px by definition) and belong to print stylesheets. A relative unit resolves against a reference that can change, which is what lets it adapt.

The reference is the whole game, so it is worth naming what each relative unit points at. rem and em are font-relative: they resolve against a font size (the root's or the element's). % is parent-relative for most properties: a percentage width is a fraction of the containing block's width. vw and vh are viewport-relative: a fraction of the browser viewport (the visible area of the page). The practical rule falls out of this: size type and layout in relative units, keep px for what should not scale. A reader who sets a larger default font size, or zooms the page, moves the reference that relative units point at, so the page grows to meet them; a px value has no reference to move and stays put, which is the accessibility failure you are trying to avoid.

JunoAbsolute vs relative units Absolute units like px are a fixed size that never changes. Relative units scale to something else, like the reader's font size or the screen width. Relative units let the page adapt to the person looking at it, which is why they matter for readers who set a larger text size.
JunoAbsolute vs relative unitspx is the one absolute unit you will actually use; everything else scales to a reference. rem and em scale to a font, % to the parent, vw and vh to the viewport. Reach for relative units so the page grows when a reader bumps up their default font size, instead of ignoring them.
JunoAbsolute vs relative units Every unit is a number plus an anchor, and the anchor decides whether the layout can respond. px has no anchor to move, so it never adapts; rem and em anchor to a font, % to the parent, vw and vh to the viewport. Size type and layout in relative units and keep px for the parts that should stay fixed.

Pixels and when they are fine

A pixel (px) is a small fixed unit of length. It is the most direct one to reason about, because 16px is always 16px. That makes it a good fit for small things that should stay the same size no matter what: the thickness of a border, a tiny rounded corner, a fixed icon.

css
.card {
  border: 1px solid #ccc;      /* a hairline border, always 1px */
  border-radius: 8px;          /* a fixed rounded corner */
}

The catch is that a pixel does not care about the reader. If someone has set their browser to show larger text, a heading you locked to 24px stays at 24px and ignores their choice. That is why px is a fine choice for borders and small details, but not the unit you want for text.

px is the unit to reach for when you want a length that stays put: borders, hairline dividers, small fixed radii, and the occasional icon that should not scale. For those, its fixedness is the point. A 1px border that grew with the reader's font size would look wrong, so pinning it is correct.

css
.card {
  border: 1px solid #ccc;      /* stays a hairline regardless of font size */
  border-radius: 8px;
  padding: 1rem;               /* but interior spacing scales with type */
}

The downside is the reason not to size text in it. A reader can set a larger default font size in their browser for comfort or accessibility, and px overrides that: a font-size: 18px heading renders at 18 pixels whether their default is 16 or 24. So use px for the fixed details and switch to a font-relative unit for anything that should honour the reader's settings, which the next section covers.

The CSS px is not a physical device pixel; it is a reference pixel, an angular unit the browser scales so 1px reads at a consistent apparent size across screen densities. On a high-density display, one CSS pixel maps to several device pixels, which is why a 1px border stays a crisp hairline rather than vanishing. That fixedness makes px the right call for things that should not track the reader's font size: borders, 1px dividers, small radii, box-shadow offsets, and hairline detailing.

css
.card {
  border: 1px solid #ccc;      /* fixed hairline; scaling it would look wrong */
  border-radius: 8px;
  padding: 1rem;               /* interior spacing tracks the type instead */
}

The failure mode is sizing text in px. Setting font-size in pixels overrides the reader's default font size, the base size they can set in browser preferences (often for low vision), because a px value has no reference to that preference to scale against. Note the distinction from page zoom: modern browsers do scale px on full-page zoom, so the old "px ignores zoom" claim is dated, but a px font size still ignores the default-font-size setting specifically, and that setting is the accessibility path many readers rely on. The rule that holds up: px for fixed detail, a font-relative unit for anything a reader should be able to enlarge, especially body text and typography.

JunoPixels and when they are fine A pixel (px) is a fixed size, so 16px is always 16px. That makes it a good fit for small things that should stay put, like a 1px border or a small rounded corner. The trade-off is that px ignores a reader who has set larger text, so it is not the unit you want for the words on the page.
JunoPixels and when they are fine Use px when a length should not scale: borders, hairline dividers, small radii. Its fixedness is the feature there. The downside shows up with text, where a px font size overrides a reader's larger default, so keep px for the fixed details and size text in a font-relative unit.
JunoPixels and when they are fine The CSS px is a reference pixel the browser keeps visually consistent across screen densities, which is why a 1px border stays a clean hairline. That makes it right for fixed detail like borders and shadows. It is wrong for text, because a px font size ignores the reader's default-font-size setting, so size type in rem instead.

em and rem

Two relative units scale to a font size: em and rem. The one to reach for is rem. It measures against the page's root font size, which is the base text size set on the whole document (16px by default). So 1rem is 16px, 1.5rem is 24px, and 2rem is 32px.

css
h1 {
  font-size: 2rem;             /* 2 x 16px = 32px */
}
p {
  font-size: 1rem;             /* 16px, the base size */
  margin-bottom: 1.5rem;       /* 24px of space below */
}

Because rem is tied to that one root size, it is predictable: 1.5rem means the same thing everywhere on the page. And when a reader raises their default text size, everything sized in rem grows together. The other unit, em, scales to the element's own font size instead, which can get tangled, so start with rem and you will rarely be caught out.

em and rem are both font-relative, and the difference is which font size they measure against. rem is relative to the root font size, the size set on the <html> element (16px by default), so 1.5rem resolves to the same length anywhere on the page. em is relative to the element's own font size, so its value shifts depending on where you use it.

css
h1 {
  font-size: 2rem;             /* always 2 x the root = 32px */
}
.card {
  font-size: 1.125rem;         /* 18px */
  padding: 1em;                /* 1 x this element's 18px = 18px */
}

The recommendation is to size in rem and use em only for scaling tied to the local font size. em works well for things that should track their own text, like padding inside a button that grows with the button's font size. But em compounds through nested elements, which surprises people, so keep it for those local cases and let rem carry your general sizing and typography.

em and rem are both font-relative, meaning they resolve against a font size rather than a fixed length, and the distinction is the reference. rem (root em) resolves against the root font size, the font-size computed on the <html> element, so it is a single global reference: 1.25rem is the same computed length in a heading, a footer, or six levels deep. em resolves against the current element's font-size, so its meaning is local and moves with context.

That local reference is where em compounds. Because a child's em is measured against its own font-size, and font-size itself inherits, setting font-size in em on nested elements multiplies at each level:

css
/* Each list level sets font-size in em, so sizes stack multiplicatively */
ul { font-size: 0.9em; }       /* 0.9 x parent */

/* A 16px root becomes:
   depth 1 -> 14.4px
   depth 2 -> 12.96px  (0.9 x 0.9)
   depth 3 -> 11.66px  (0.9 x 0.9 x 0.9)  */

Nobody asked for text that shrinks the deeper it nests; that is em compounding, and it is the main argument for reaching for rem by default. The trade-off runs the other way for component-local scaling, where you want a value to track one element's own type: a button whose padding is 0.75em and border-radius 0.5em rescales as a unit when you bump its font-size, which rem cannot do because it ignores the local size. So the working split is rem for anything that should stay stable against the root, em for spacing and radii that should scale with their own element's font size.

For fluid sizing, min(), max(), and clamp() let a length flex between bounds. min(a, b) picks whichever is smaller at the current viewport, max(a, b) the larger, and clamp(min, preferred, max) holds a preferred value between a floor and a ceiling. A common pattern for a heading is font-size: clamp(1.75rem, 4vw, 3rem): it scales with the viewport (4vw) but never drops below 1.75rem or exceeds 3rem, so the type stays fluid without a stack of breakpoints. Sizing type in rem (or rem-bounded clamp()) also respects user zoom and the reader's default font size, because those adjust the root the rem resolves against, whereas a raw vw value tracks only the viewport and ignores the reader's font preference. That is why the rem floor and ceiling in a clamp() matter: they keep the accessible reference in the expression.

Junoem and remrem and em both scale to a font size, and the one to start with is rem. It measures against the page's root size, which is 16px by default, so 1.5rem is 24px and means the same thing everywhere. em scales to the element's own font size instead, which can get tangled, so reach for rem first.
Junoem and rem Both are font-relative: rem measures against the root font size, so it is stable everywhere, while em measures against the element's own font size, so it shifts with context and compounds when nested. Size in rem by default and keep em for spacing that should track its own element's type, like padding inside a button.
Junoem and remrem anchors to the root font size for one stable reference; em anchors to the element's own size and multiplies through nesting, which is why deeply nested em font sizes drift. Use rem for general sizing and em for component-local scaling that should follow its own type. For fluid type, clamp(1.75rem, 4vw, 3rem) flexes with the viewport while the rem floor and ceiling keep it respecting user zoom and font settings.

Percentages

A percentage (%) sizes an element against its parent. For width, width: 50% means half the width of the box the element sits inside, whatever that width happens to be.

css
.sidebar {
  width: 25%;                  /* a quarter of the parent's width */
}
.content {
  width: 75%;                  /* the remaining three quarters */
}

This is what makes a layout fluid: because the widths are fractions of the parent, they shrink and grow as the parent does. Resize the window and a 50% column stays half the width instead of spilling over the edge the way a fixed 500px column would. That flexibility is why percentages show up so often in responsive design.

A percentage resolves against a reference on the parent, and for width that reference is the parent's content width. So width: 50% gives you half of whatever the containing box currently is, which is what makes percentage-based columns fluid: they track the parent as it resizes.

css
.container {
  width: 90%;                  /* 90% of its parent, up to a cap below */
  max-width: 60rem;            /* stop growing past 60rem on wide screens */
  margin: 0 auto;              /* centre it */
}

The reference is not always width, and that trips people up. A percentage padding or margin, even the vertical ones, resolves against the parent's width, not its height. A percentage height resolves against the parent's height, but only if the parent has a definite height to measure against, otherwise it has nothing to resolve to and is ignored. Pairing a percentage width with a max-width in rem, as above, is the common move: fluid up to a point, then capped so lines do not stretch too wide to read.

A percentage is relative, but unlike rem and em its reference is not a font size, it is a dimension of the containing block (the box that establishes the reference for a positioned or in-flow element, usually the parent's content box). The catch that bites is that the reference dimension is property-specific, and not always the one you would expect:

css
.hero {
  width: 80%;                  /* 80% of the containing block's WIDTH */
  padding-top: 25%;            /* also 25% of the WIDTH, not the height */
  margin-bottom: 10%;          /* WIDTH again, even though it is vertical */
}

width, padding, and margin percentages all resolve against the containing block's width, including the vertical paddings and margins. That width-based vertical padding is not a quirk to route around; it is the classic trick for a fixed aspect-ratio box before aspect-ratio shipped, since padding-top: 56.25% gives a 16:9 area that scales with width. A height percentage resolves against the containing block's height, but only if that height is definite (explicitly set or otherwise resolvable); against an auto height it computes to auto and appears to do nothing, which is the usual reason a percentage height silently fails. In modern layout, flex-basis and grid track percentages behave more predictably, so percentages stay useful for fluid widths while fr units and min()/max()/clamp() cover the cases percentages handle awkwardly.

JunoPercentages A percentage sizes against the parent, so width: 50% is half the width of the box the element sits in. That makes columns fluid: resize the window and they grow and shrink instead of spilling over. It is one of the main tools behind layouts that fit any screen.
JunoPercentages A percentage width is a fraction of the parent, which is what keeps columns fluid as the container resizes. Watch the reference: percentage padding and margin resolve against the parent's width, even the vertical ones, and a percentage height needs the parent to have a definite height. Pairing a percentage width with a max-width in rem keeps it fluid but capped.
JunoPercentages Percentages resolve against the containing block, and the reference is property-specific: width, padding, and margin all use its width, vertical values included, which is the trick behind width-based aspect-ratio boxes. A percentage height needs a definite parent height or it computes to auto and does nothing. Keep percentages for fluid widths and let fr units and clamp() handle the awkward cases.

Viewport units

Viewport units size against the browser window itself. vw is a percentage of the viewport width and vh is a percentage of the viewport height, where the viewport is the visible area of the page. So 100vw is the full width of the window and 100vh is its full height.

css
.hero {
  height: 100vh;               /* fills the whole screen height */
}

This is the go-to for a full-screen section, like a landing banner that should fill the window whatever size it is. Because the units track the window, the section stays full-screen when someone resizes or rotates their device, without you setting a fixed pixel height that would only be right on one screen.

Viewport units measure against the viewport, the visible area of the browser window. vw is one percent of the viewport width and vh is one percent of its height, so 100vh is a full window's height and 50vw is half its width. They are the natural fit for full-screen sections and hero banners that should fill the window at any size.

css
.hero {
  min-height: 100vh;           /* at least a full screen tall */
  padding: 2rem;
}

One thing to know on mobile: 100vh historically included the space behind the browser's address bar, so a full-height section could sit taller than the visible area and cut off at the bottom. The fix is dvh, the dynamic viewport height, which tracks the actual visible height as the browser chrome shows and hides. Reach for min-height: 100dvh on mobile full-screen sections, and note that raw vw on font sizes ignores the reader's font settings, so keep viewport units for layout rather than type.

Viewport units resolve against the viewport, the visible rendering area of the browser window. vw and vh are one percent of its width and height; vmin and vmax are one percent of the smaller and larger of the two, handy for values that should respond to orientation. Their reference is the window, not any parent, so they are the right tool for full-viewport sections independent of the layout around them.

css
.hero {
  min-height: 100dvh;          /* full visible height, mobile-safe */
  padding: 2rem;
}

The sharp edge is mobile, where the viewport height is not one number. The browser's address bar and toolbars expand and collapse as the user scrolls, so 100vh was pinned to the largest possible viewport and a full-height section would overflow behind the chrome. CSS now exposes three: svh (small viewport height, chrome expanded), lvh (large, chrome collapsed), and dvh (dynamic, which follows the current state live). Use dvh for full-screen sections that must fit the visible area, accepting that it reflows as the bars move. The other trap is font-size in raw vw: because a viewport unit's reference is the window and not the root font size, font-size: 4vw ignores both the reader's default font size and page zoom that does not change the viewport, which fails the same accessibility test as a px font size. The fix is to bound it: clamp(1rem, 4vw + 0.5rem, 2rem) keeps the fluid behaviour while the rem terms restore a reference the reader's settings can move.

JunoViewport unitsvw and vh size against the browser window: 100vw is its full width and 100vh is its full height. They are the go-to for a full-screen section like a landing banner, because it stays full-screen when the window resizes instead of being locked to one pixel height.
JunoViewport unitsvw and vh are one percent of the window's width and height, which makes them right for hero sections that should fill the screen at any size. On mobile, 100vh can run taller than the visible area behind the address bar, so reach for dvh there. Keep viewport units for layout, not font sizes, since raw vw on type ignores the reader's settings.
JunoViewport units Viewport units anchor to the window, not a parent, so they suit full-viewport sections; vmin and vmax respond to orientation. On mobile the height is not one value, so use dvh to follow the visible area as the browser chrome moves. Never size type in raw vw, since it ignores the reader's font settings; bound it in a clamp() with rem terms so an accessible reference stays in the expression.