You're 40 minutes deep into a component. Figma says font-size: 24px. You open a calculator, type 24 / 16, get 1.5, switch back to VS Code, type 1.5rem, close the calculator.
Three minutes later you need 36px. Calculator again. 36 / 16... is that 2.25? Let me double check...
Sound familiar? I've been there more times than I can count. And honestly — it's one of those tiny frictions that doesn't feel like a big deal until you realize you've done it 20 times in a single session.
Why We're Still Doing This Manually
The formula is stupid simple:
rem = px ÷ base font size
px = rem × base font size
With a 16px base (browser default):
24px ÷ 16 = 1.5rem ✓
36px ÷ 16 = 2.25rem ✓
14px ÷ 16 = 0.875rem ✓
So yeah, the math isn't hard. The problem is context switching. Every time you leave your editor to calculate something, you're paying a mental overhead cost. Stack that 15–20 times per session and you've lost a solid chunk of deep work time.
The Quick Reference You Actually Need
Here are the values I reach for constantly — with a standard 16px base:
| px | rem |
|---|---|
| 10px | 0.625rem |
| 12px | 0.75rem |
| 14px | 0.875rem |
| 16px | 1rem |
| 18px | 1.125rem |
| 20px | 1.25rem |
| 24px | 1.5rem |
| 28px | 1.75rem |
| 32px | 2rem |
| 36px | 2.25rem |
| 40px | 2.5rem |
| 48px | 3rem |
| 56px | 3.5rem |
| 64px | 4rem |
| 80px | 5rem |
| 96px | 6rem |
Bookmark this section. Seriously.
Wait — Why rem At All?
If you're newer to CSS, you might be wondering why we bother with rem in the first place. Pixels work fine, right?
Here's the thing: px is an absolute unit. When you write font-size: 16px, that's always 16 pixels, regardless of what the user has set in their browser preferences.
rem is relative to the root <html> element's font size — which browsers default to 16px, but users can change. That one difference matters a lot for accessibility.
/* User sets browser font to "Large" (say, 20px) */
p { font-size: 16px; } /* still renders at 16px — ignores user */
p { font-size: 1rem; } /* renders at 20px — respects user */
WCAG 2.2 criterion 1.4.4 (Resize Text) requires that users can resize text up to 200% without breaking layout or losing content. rem handles this automatically. px doesn't.
Beyond accessibility, rem makes your spacing system predictable. Change the root font size once — everything scales proportionally.
The html { font-size: 62.5% } Trick (And Why It's Outdated)
You've probably seen this pattern in older codebases:
html {
font-size: 62.5%; /* Makes 1rem = 10px */
}
h1 { font-size: 3.6rem; } /* = 36px */
p { font-size: 1.6rem; } /* = 16px */
The appeal: round numbers. 36px = 3.6rem feels cleaner than 2.25rem.
The problem: setting font-size: 62.5% on html overrides the user's browser preference before anything can respond to it. You're essentially opting out of accessibility at the root level.
Modern best practice is to leave the html font size alone (or use 100%) and let 1rem = user's preference. Then use a converter tool to do the math — not a base-size hack.
Dealing With a Non-Standard Base in Your Project
Not every project uses 16px. You might inherit a codebase with:
html { font-size: 10px; }
/* or */
html { font-size: 18px; }
In these cases the standard conversion tables are wrong. 24px is NOT 1.5rem if your base is 10px — it's 2.4rem.
This is one reason I always use a proper px → rem converter with a configurable base rather than relying on memory or hardcoded tables. When I'm working on a project with a custom root font size, I just update the base field and every value recalculates correctly.
→ Free PX to REM Converter — bidirectional, custom base, bulk conversion
My Actual Workflow: Converting a Full Type Scale
When I'm starting a new design system or setting up typography tokens, I use the bulk converter instead of doing values one by one.
Say your designer gave you this type scale in Figma (all in pixels):
12 / 14 / 16 / 18 / 20 / 24 / 30 / 36 / 48 / 60 / 72
Throw all 11 values into the bulk converter, hit "Copy All", and you get:
12px = 0.75rem
14px = 0.875rem
16px = 1rem
18px = 1.125rem
20px = 1.25rem
24px = 1.5rem
30px = 1.875rem
36px = 2.25rem
48px = 3rem
60px = 3.75rem
72px = 4.5rem
Paste directly into your CSS custom properties or Tailwind config. Done in 30 seconds instead of 5 minutes.
:root {
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
--text-5xl: 3rem; /* 48px */
}
rem in Media Queries (The Part Nobody Talks About)
Here's something that trips people up: media queries accept rem too — and it's actually the better choice.
/* Works, but doesn't scale with user font preference */
@media (min-width: 768px) { ... }
/* Scales with user preference */
@media (min-width: 48rem) { ... }
With px breakpoints: if a user bumps their font size to 20px, your layout still breaks at 768 device pixels regardless of their preference.
With rem breakpoints: the breakpoint itself scales. At 20px root, 48rem = 960px — your layout adapts. The user's preference propagates all the way through your responsive design.
Quick conversion for the standard breakpoints:
480px = 30rem
640px = 40rem
768px = 48rem
1024px = 64rem
1280px = 80rem
1440px = 90rem
1536px = 96rem
Tailwind Users: Arbitrary Values + rem
Tailwind's preset scale already uses rem internally — text-base is 1rem, text-xl is 1.25rem, and so on. But when a design spec calls for something off the preset scale, you need arbitrary values:
<h1 class="text-[2.25rem] leading-[2.75rem]">Heading</h1>
<div class="mt-[1.875rem] px-[1.5rem]">Content</div>
The converter I mentioned generates the Tailwind arbitrary value string automatically alongside the standard CSS — so you don't have to manually format it.
If you're on Tailwind v4 and building a custom @theme block, those tokens go in as rem values too:
@theme {
--font-size-display: 3rem; /* 48px */
--font-size-heading: 2.25rem; /* 36px */
--spacing-section: 5rem; /* 80px */
}
PostCSS Automation (For Large Codebases)
If you're migrating a legacy codebase with thousands of px declarations, doing it manually isn't realistic. There's a PostCSS plugin — postcss-pxtorem — that converts px to rem at build time.
npm install postcss-pxtorem --save-dev
// postcss.config.js
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 16,
propList: ['font-size', 'line-height', 'letter-spacing'],
// Leave margins/paddings in px if you want
}
}
}
You write font-size: 24px in development, it outputs font-size: 1.5rem in production. Same formula this tool uses — just automated.
Be careful with the propList — don't blindly convert everything to rem. Borders, icon sizes, and elements that should never scale usually want to stay in px.
TL;DR
- Use
remfor font sizes and spacing — it respects user browser preferences and satisfies WCAG - Use
pxfor borders, shadows, and things that genuinely shouldn't scale -
rem = px ÷ base font size(default base =16px) - Don't use the
62.5%html trick on new projects — it's an accessibility anti-pattern -
remworks in media queries too — and it's better for accessibility - For bulk conversions or a non-standard base, use a proper tool
→ WebToolsHub — PX to REM Converter (free, bidirectional, bulk)
If you use CSS custom properties for design tokens, our CSS Variables & Dark Mode guide covers how to wire up a full token system in Next.js. And if you're migrating to Tailwind from a pixel-heavy codebase, the CSS to Tailwind converter handles the class-mapping layer.
Tags: css webdev frontend beginners
Top comments (0)