Hardcoding font-size: 16px across a stylesheet looks harmless on a developer's 27-inch monitor. The problem shows up when a user with low vision sets their browser default font size from 16px to 24px in Chrome or Firefox settings: every px-locked heading and paragraph ignores their preference completely, while rem-based text scales cleanly on the first render.
That doesn't mean you should convert every single number in your CSS file to rem. Using rem for a 1px card border or using em for nested container widths creates a different mess. Each unit has a specific job in a design system.
Quick Decision Matrix: When to Use px, rem, em, or vw/vh
| CSS Unit | Relative To | Scales with User Browser Font Preference? | Best Production Use Case | Avoid Using For |
|---|---|---|---|---|
rem | Root <html> font-size (default 16px) | Yes | Global font sizes, layout max-width containers, section vertical spacing, media query breakpoints | 1px borders or inline icons that must match a specific parent's font size |
em | Current element's (or parent's) computed font-size | Yes (compounds when nested) | Button padding-inline: 1.1em, badge spacing, inline SVG icon sizing (width: 1em), letter-spacing | Multi-level nested list or grid font sizes (causes compounding shrinkage) |
px | CSS reference pixel (1/96th of an inch) | No (scales only on full-page zoom) | 1px or 2px borders, outline-offset, box-shadow blur, hairline dividers | Body copy, headings, or fixed-height text containers |
vw / svh / dvh | Viewport width or small/dynamic viewport height | No | Full-bleed hero heights (min-height: 100svh) and the fluid slope term inside clamp() | Standalone font-size: 3vw without a rem base |
The Exact PX to REM Formula (And Why Compounding Breaks em)
In every modern browser, the unstyled root <html> element defaults to 16px. Converting any pixel value from a Figma spec into rem is a single division:
PX to REM Formula: rem = target_px / base_font_size_px (at default 16px root: rem = px / 16, and px = rem * 16).
Common typographic scale steps at a 16px root:
12px=0.75rem14px=0.875rem16px=1rem18px=1.125rem20px=1.25rem24px=1.5rem32px=2rem44px=2.75rem
Why not use em for typography? Because em resolves against the parent element's font size. If you set .card { font-size: 0.875em } (14px) and put another .card or .meta with 0.875em inside it, the inner text computes to 16 * 0.875 * 0.875 = 12.25px. Add a third nesting level and your text shrinks to 10.7px. Reserve em for properties that *should* scale proportionally with the local text size of a single component, such as button padding (padding: 0.6em 1.2em).
How to Calculate Fluid Typography with clamp(min, preferred, max)
Instead of writing three @media breakpoints to step an <h1> from 28px (1.75rem) on a 375px mobile screen up to 44px (2.75rem) on a 1200px desktop viewport, CSS clamp() interpolates smoothly between the two bounds.
Linear Interpolation Formula for clamp(): slope = (max_font_px - min_font_px) / (max_viewport_px - min_viewport_px), y_intercept_px = min_font_px - (slope * min_viewport_px), and preferred = (y_intercept_px / 16)rem + (slope * 100)vw.
Let's compute the exact clamp() declaration for a heading that scales from 28px at 375px viewport width to 44px at 1200px viewport width:
/*
1. Slope = (44 - 28) / (1200 - 375) = 16 / 825 = 0.019394 (1.94vw)
2. Intercept = 28 - (0.019394 * 375) = 28 - 7.2727 = 20.7273px (1.295rem)
*/
h1 {
font-size: clamp(1.75rem, 1.295rem + 1.94vw, 2.75rem);
line-height: 1.18;
letter-spacing: -0.03em;
}Notice that the middle preferred argument is 1.295rem + 1.94vw, not a bare vw number. Keeping the 1.295rem intercept inside the preferred expression guarantees that the heading still respects user font-size zooming under WCAG 1.4.4.
Aligning Line-Height and Spacing to a Vertical Baseline Grid
When you convert font sizes to rem, pair them with unitless line-height values or rem multiples that snap to a 4px (0.25rem) or 8px (0.5rem) vertical rhythm. For example, 16px (1rem) body text with a 1.75 line-height produces a 28px (1.75rem) line box, which divides cleanly into a 4px sub-grid.
Once your design tokens and fluid clamp() rules are finalized, strip comments and whitespace from production stylesheet bundles before deployment.