zudo-css-wisdom
GitHub repository

Type to search...

to open search from anywhere

px vs rem

What rem still buys after Internet Explorer — the browser default font size preference, which dimensions should follow it, and where px is the right unit.

The Problem

The classic argument for rem was Internet Explorer: IE could not enlarge text set in px, so relative units were the only way a user could resize text. IE is gone. Every current browser page-zooms px and rem identically, which leads to a common conclusion — "px is fine now, and 16px reads more directly than 1rem."

That conclusion skips one mechanism that still exists: the browser default font size preference. Chrome, Edge, and Firefox let a user set the root font size to 20px, 24px, or 32px as a persistent setting, separate from per-site zoom. Text in rem follows that setting. Text in px does not. A site set entirely in px renders at 16px for a user who asked for 24px — before they reach for zoom on every site.

The opposite mistake is also common: once a team adopts rem, every value becomes rem, including borders, shadows, and corner radii. At a 24px root, a 0.0625rem border becomes 1.5px, which browsers snap to whole device pixels — 1px on a 1× screen, 3px on a 2× screen — so hairlines change thickness per device; a 0.25rem spacing scale makes the entire interface 50% larger, which eats the line width that enlarged text needed in the first place. Alongside these, the html { font-size: 62.5% } trick — "so 1rem = 10px" — silently shrinks every piece of third-party or unstyled 1rem text to 10px.

The Solution

Choose the unit by asking one question: should this value grow when the user enlarges their preferred text size?

AnswerUnitTypical values
Yes — it is text, or it exists because of textremfont-size, spacing scale, vertical rhythm, min-height of a text control
Yes — but relative to this element's own textemletter-spacing, inline icon size, button padding that should follow a font-size override
No — it is a visual detail or a geometric constraintpxborders, outline, box-shadow, border-radius, hairlines, raster sizes
It is neither — it is a ratiounitless / % / chline-height, reading measure, percentage widths

Leave the root font size alone — html { font-size: 100% } or nothing. rem then means "the user's chosen base size", which is the only reason to prefer it over px.

Code Examples

What the Default Font Size Setting Does

This demo simulates the browser setting by changing the root font size — the same mechanism Chrome's "Font size" preference uses. Both columns are authored at the same visual size. Only the rem column responds.

Default font size 16 / 20 / 24px — px text ignores it, rem text follows it

Page Zoom Is Not the Same Control

Page zoom and default font size are independent settings with different effects:

User action16px text1rem text (root untouched)Layout
Page zoom 200% (Cmd/Ctrl +)DoublesDoublesViewport shrinks in CSS px, breakpoints re-evaluate
Default font size 16 → 24px (Chrome, Edge, Firefox)Unchanged1.5×Viewport unchanged; rem/em breakpoints re-evaluate, px breakpoints do not
Firefox "Zoom text only"Browser-dependent; usually both enlargeEnlargesViewport unchanged
Minimum font size (Safari, Firefox)FlooredFloored

Page zoom covers the IE-era problem: px text is no longer un-resizable. The default font size preference is the remaining gap, and it is where rem earns its place. Safari on macOS does not expose a general default font size — only a minimum — so a Safari-only test will not reveal px text ignoring the preference.

How many users change the default? The only measured general-population figure is old: about 3% of Internet Archive visitors in 2018 had a root size other than 16px. Surveys of low-vision users report much higher rates (8–27% depending on the population). WCAG 1.4.4 (Resize Text) does not require rem — full-page zoom is a listed sufficient technique — so px text is not an automatic failure. The case for rem is that supporting the preference costs nothing at kickoff and something at retrofit time.

Which Dimensions Should Follow the Text

Scaling text without scaling the space around it produces cramped components; scaling everything produces device-dependent border thickness and oversized shadows and radii. The demo puts three cards under the same simulated preference.

All px vs all rem vs mixed — simulate a 24px default font size

A rem Spacing Scale — the Tailwind v4 Default

Tailwind CSS v4 derives every spacing utility from one token: --spacing: 0.25rem, so p-4 is calc(var(--spacing) * 4) = 1rem = 16px at the default root. Its text sizes are rem too, and its v4 breakpoints moved from 640px to 40rem. The escape hatches stay in px where px belongs: border is 1px, p-px exists, and arbitrary values like [4px] are allowed.

:root {
  --spacing: 0.25rem; /* Tailwind v4 */
}

.p-4 {
  padding: calc(var(--spacing) * 4); /* 1rem — 16px at a 16px root, 24px at a 24px root */
}

The trade-off of a rem spacing scale is that it also drives widths and heights, so a larger browser default behaves like a second, partial page zoom: gaps and paddings take more of the line. That is usually acceptable — text that grew needs more breathing room — but horizontal page gutters and fixed-width chrome are the places to consider px or a clamp() with a px term instead. If the team later decides the scale should be fixed, changing one token (--spacing: 4px) switches every utility without touching markup.

Breakpoints: rem and em Read the Browser's Initial Font Size

In a media query, rem and em resolve against the browser's initial font size — the user preference — not against whatever html { font-size } the stylesheet sets. A user with a 24px default and a 1280px window hits a 48rem breakpoint at 1152px of CSS width, so the layout switches to its narrower form when enlarged text reduces effective space. A 768px breakpoint never does.

/* Geometry-based: fires at 768 CSS px regardless of text size */
@media (min-width: 768px) { }

/* Text-capacity-based: fires at 768px with a 16px default,
   at 1152px with a 24px default */
@media (min-width: 48rem) { }

The old reason to avoid em/rem breakpoints was a Safari zoom bug. Safari 15 fixed the commonly cited case, and Tailwind v4 switched its defaults to rem in 2024 on that basis. Container queries differ: @container (inline-size > 40em) resolves em against the container's own font size, while cqi units track geometry only — see Container Queries for why a cqi-only font size ignores text scaling.

The 62.5% Trick

html { font-size: 62.5%; }   /* 1rem = 10px at a 16px default */
body { font-size: 1.6rem; }  /* back to 16px */

Because 62.5% is still relative, this does not break the user preference — a 20px default produces a 12.5px root and 20px body text. It is discouraged for a different reason: it redefines 1rem for everything that did not opt in. Third-party widgets, browser-extension UI, and any unstyled 1rem text render at 10px.

html { font-size: 62.5% } shrinks every 1rem value that did not compensate

The rem is already set to the browser's default size, so a base of 1rem needs no adjustment. Let the math live in tokens, not in the root font size. Never set html { font-size: 10px } either — an absolute value there overrides the preference directly.

Kickoff Setup

/* Leave the root alone. Omit entirely, or make the intent explicit. */
html {
  font-size: 100%;
}

:root {
  /* Text and text-driven rhythm — follow the preference */
  --font-body: 1rem;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-8: 2rem;

  /* Visual details — do not follow the preference */
  --border-width: 1px;
  --focus-width: 2px;
  --radius-sm: 6px;
  --shadow-sm: 0 2px 6px hsl(215, 30%, 20%, 0.15);
}

body {
  font-size: var(--font-body);
  line-height: 1.5;          /* unitless ratio */
}

h1 {
  font-size: clamp(2rem, 1.5rem + 2vw, 3rem); /* rem floor and rem share in the preferred term */
  letter-spacing: -0.02em;   /* relative to its own size */
}

.button {
  min-block-size: 44px;      /* touch target: a physical constraint */
  padding: 0.625em 1em;      /* follows the button's own font-size */
  border: var(--border-width) solid currentColor;
  border-radius: var(--radius-sm);
}

.button > svg {
  inline-size: 1em;          /* inline icon: follows the text */
  block-size: 1em;
}

.prose {
  max-inline-size: 65ch;     /* reading measure: ch */
}

.page-shell {
  inline-size: min(100% - 32px, 1200px); /* page geometry: px */
  margin-inline: auto;
}

@media (min-width: 48rem) { }

Test at the end of setup, not only at the end of the project: set the browser default font size to 24px in Chrome or Firefox, check that text and spacing grew and borders did not, then page-zoom to 200% and 400% for WCAG 1.4.4 and 1.4.10.

Quick Reference

ScenarioTechnique
Body, heading, label font sizesrem
Spacing scale, vertical rhythmrem (or em when tied to a specific element's size)
Button/input padding, inline iconsem — follows the component's own font-size
letter-spacingem
line-heightunitless
Borders, outlines, hairlinespx
box-shadow, border-radius on fixed shapespx
Touch target minimumspx (44px)
Reading column widthch with max-width: 100%
Page-shell max width, gutterspx or % — geometry, not text
Viewport breakpointsrem (or em — identical in media queries)
Container breakpointsem relative to the container's font size
Root font sizeuntouched / 100% — never 62.5%, never 10px

Common AI Mistakes

  • Treating "IE is gone" as "px is equivalent to rem" — page zoom is equal, but the browser default font size preference is still a separate, persistent setting that px ignores

  • Converting every value to rem — borders become fractional at a 24px root and snap to different device-pixel widths per screen, shadows and radii swell, and a rem spacing scale eats line width that enlarged text needed

  • Setting html { font-size: 62.5% } for round numbers — third-party and unstyled 1rem text drops to 10px; put the arithmetic in tokens instead

  • Setting html { font-size: 16px } or body { font-size: 16px } as a "reset" — an absolute root overrides the user preference entirely; an absolute body size cuts off inheritance for everything inside it

  • Using px breakpoints alongside rem text — enlarged text never triggers the narrower layout; use rem or em in the media query

  • Expecting html { font-size } to change rem breakpoints — media query units resolve against the browser's initial font size, not author styles on html

  • Claiming WCAG requires rem — 1.4.4 lists page zoom as a sufficient technique; the case for rem is the preference, not conformance

  • Using em for font sizes in nested components — sizes compound through ancestors; use rem for font-size and reserve em for dimensions that should track that font size

When to Use

rem

Font sizes, the spacing scale, vertical rhythm, and any dimension that exists because of text — a min-height on an input, a max-width on a dialog that holds prose.

em

Values that should follow the element's own font-size override: button padding, inline icon size, letter-spacing, margins in a prose component sized by heading level.

px

Borders, outlines, shadows, radii on fixed shapes, touch-target minimums, page-shell geometry, and anything that would look wrong thicker or larger when text is enlarged. px font sizes are defensible only inside fixed compositions — canvas annotations, slide stages — where Stage-Proportional Typography already covers the trade-off.

Never

html { font-size: 62.5% }, html { font-size: 10px }, or a px font-size on body as a reset. Leave the root at the browser default so rem means what it is meant to mean.

References

Revision History

CreatedUpdated