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?
| Answer | Unit | Typical values |
|---|---|---|
| Yes — it is text, or it exists because of text | rem | font-size, spacing scale, vertical rhythm, min-height of a text control |
| Yes — but relative to this element's own text | em | letter-spacing, inline icon size, button padding that should follow a font-size override |
| No — it is a visual detail or a geometric constraint | px | borders, outline, box-shadow, border-radius, hairlines, raster sizes |
| It is neither — it is a ratio | unitless / % / ch | line-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.
Page Zoom Is Not the Same Control
Page zoom and default font size are independent settings with different effects:
| User action | 16px text | 1rem text (root untouched) | Layout |
|---|---|---|---|
| Page zoom 200% (Cmd/Ctrl +) | Doubles | Doubles | Viewport shrinks in CSS px, breakpoints re-evaluate |
| Default font size 16 → 24px (Chrome, Edge, Firefox) | Unchanged | 1.5× | Viewport unchanged; rem/em breakpoints re-evaluate, px breakpoints do not |
| Firefox "Zoom text only" | Browser-dependent; usually both enlarge | Enlarges | Viewport unchanged |
| Minimum font size (Safari, Firefox) | Floored | Floored | — |
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.
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.
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
| Scenario | Technique |
|---|---|
| Body, heading, label font sizes | rem |
| Spacing scale, vertical rhythm | rem (or em when tied to a specific element's size) |
| Button/input padding, inline icons | em — follows the component's own font-size |
letter-spacing | em |
line-height | unitless |
| Borders, outlines, hairlines | px |
box-shadow, border-radius on fixed shapes | px |
| Touch target minimums | px (44px) |
| Reading column width | ch with max-width: 100% |
| Page-shell max width, gutters | px or % — geometry, not text |
| Viewport breakpoints | rem (or em — identical in media queries) |
| Container breakpoints | em relative to the container's font size |
| Root font size | untouched / 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
pxignoresConverting 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 aremspacing scale eats line width that enlarged text neededSetting
html { font-size: 62.5% }for round numbers — third-party and unstyled1remtext drops to 10px; put the arithmetic in tokens insteadSetting
html { font-size: 16px }orbody { font-size: 16px }as a "reset" — an absolute root overrides the user preference entirely; an absolutebodysize cuts off inheritance for everything inside itUsing
pxbreakpoints alongsideremtext — enlarged text never triggers the narrower layout; useremoremin the media queryExpecting
html { font-size }to changerembreakpoints — media query units resolve against the browser's initial font size, not author styles onhtmlClaiming WCAG requires
rem— 1.4.4 lists page zoom as a sufficient technique; the case forremis the preference, not conformanceUsing
emfor font sizes in nested components — sizes compound through ancestors; useremforfont-sizeand reserveemfor 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
The Surprising Truth About Pixels and Accessibility — Josh W. Comeau
Pixels vs. Ems: Users DO Change Font Size — Evan Minto, Internet Archive
Don't use em for media queries (and the Safari 15 update) — Adam Wathan