Skip to content
Code2026-10-21Begin5 min read

px, rem, em: What Each CSS Unit Actually Answers, and How to Convert Without Surprises

font-size: 16px and font-size: 1rem render identically for most users, which is how the "px and rem are interchangeable" myth survives. They answer different questions: px asks "how many pixels", rem asks "how many multiples of the root font size" — and the root follows the user's browser settings, while px ignores them. Once the boundary between the three units is clear, conversion is grade-school math; when it isn't, no formula will save you. For the arithmetic itself, the px ↔ rem converter handles both directions with a customizable root; pt/vw/em conversions live in the CSS unit converter.

The boundary line

UnitRelative toChanges whenUse it for
pxdevice pixels (CSS reference pixel)nothing font-related1px dividers, box-shadow offsets, hairlines
remthe root element's font-sizeuser changes browser default size, or you change html{font-size}typography at large: font sizes, spacing, radii, breakpoints
emthe current element's font-size (inherited)any ancestor's font changesrelative spacing within a component

One-line memory hook: rem for global consistency, em for local proportion, px for physical fixation.

The conversion: formula and size table

rem = px / root-font-size      (root defaults to 16px)
px  = rem × root-font-size

Common values at root = 16 (this table ships inside the converter, annotated by role):

pxremRole
120.75fine print
140.875compact body
161root baseline
181.125subheading
241.5heading
322large heading
483display

A precision detail: px→rem often yields infinite decimals (15/16 = 0.9375); 3–4 places are plenty, the browser parses no deeper. The reverse is always finite (rem × 16), so 2 places suffice.

Why typography bets on rem: the accessibility hard requirement

The browser's font-size setting (Chrome → Settings → Appearance) changes the default root font-size. A low-vision user raising it from 16 to 20 gets:

  • an all-rem site: everything scales in proportion, layout intact;
  • an all-px site: the setting does nothing;
  • worst case, a rem/px mix: only the text grows while containers and spacing stay fixed — overflow, broken layout.

That's the test for "which properties deserve rem": anything that should track the user's reading size (font sizes, line heights, paddings, margins, text-container widths) goes rem; physical decoration (1px lines, shadow offsets) stays px. The browser minimum-font-size mechanism adds another reason — see the FAQ.

The em compounding trap

em resolves against the current element's font-size, which itself may inherit — nested lists are the classic incident:

li { font-size: 1.2em; }  /* each level grows 20%: 1.2 × 1.2 × 1.2 … */

Three levels deep: 16 × 1.728 ≈ 27.6px, and the list visibly balloons. The same mechanism makes em-based paddings context-dependent — sometimes a feature (proportional rhythm), sometimes the bug that breaks alignment. Debugging technique: read the computed font-size in DevTools and walk the chain root-to-leaf until the multiplier stops matching your mental model.

The html { font-size: 62.5% } school: convenience and its price

Setting root to 10px makes conversion a decimal shift (2.4rem = 24px) with zero mental cost. The price: you've severed the browser default connection — users who deliberately set 20px/24px roots get slammed back to 10px, a straight accessibility regression; and some older engines clamp roots below 12px to their minimum, quietly distorting the 10px baseline.

The middle path: leave the root alone and outsource the arithmetic to tooling — editor extensions or a build-time PostCSS px2rem step. Mental math to the tools, accessibility dividend to the users.

The media-query rem trap

@media (min-width: 40rem) resolves rem against the root too — but Safari historically pins rem to 16px inside media queries, ignoring your overridden html. On a 62.5% site (root = 10px), the 40rem breakpoint is 400px in most browsers and 640px in Safari — a 240px divergence that forks your layout per engine. It's a second arrow in the 62.5% school's back, and the practical argument for "breakpoints in px or vw, content in rem".

Implementer's note

The site's converters are deliberately pure functions: pxToRem(px, root) keeps 4 decimals and trims trailing zeros (0.7500 → 0.75 — what you copy is a clean literal), remToPx keeps 2 (pixels don't need more). The root field is adjustable with a floor of 1 — 10 is a legal input (the 62.5% school is real), 0 or negative throws instead of dividing silently. The dual inputs stay linked but never locked: a hand-edited side isn't stomped by the next keystroke on the other — changed after annoying myself with it while writing docs. The role annotations in the size table (fine print / body / heading) follow Chinese web-typography conventions; the numeric values coincide with English type scales even where the naming doesn't.

FAQ

Is rem supported everywhere?

IE9+ shipped it; in 2026 support is a non-question. The only live edge is Safari's media-query rem behavior described above.

Does Chrome's minimum font size break rem layouts?

Chrome enforces a 6px floor and applies its font-size setting to some UI in special ways — under extreme settings, rem-sized text may render above its computed value. That's accessibility working as intended, not a bug. Layouts that survive such inflation are exactly what rem plus flexible containers buys you.

Line height: em or unitless?

Unitless (e.g. line-height: 1.5), always: it scales per-element and inherits as a ratio, while an em line-height bakes the computed value into inheritance — children whose font-size changes keep the stale height. A notorious inheritance trap worth memorizing on its own.

My design mockups are all px. How do I migrate to rem wholesale?

Divide by 16 (batch via the converter) or wire in a PostCSS px2rem step at build time. While you're at it, snap the spacing system to a 4/8 grid — otherwise the rem values will sprout ugly decimals forever.


These unit-boundary notes come from building the converters at ToolVault; the table and the link behavior are real implementation tradeoffs, not filler.

Field note

The dual-input sync was originally hard-wired (every edit stomped the other side) until writing this very documentation had me re-typing 1.1 → 1.125 three times through stomped intermediates — the most valuable QA engineer for a tool is always its first real user.