From PX to REM: Unit Conversion in Responsive Design
From PX to REM: Unit Conversion in Responsive Design#
I inherited a legacy project with hardcoded px values everywhere. The product team wanted user-customizable font sizes. I opened the CSS files—2,000+ px values staring back at me. That was the day I truly understood the value of rem.
I built a PX/REM Converter to handle this, and here’s what I learned.
What REM Actually Means#
rem stands for “root em”—relative to the root element’s font-size.
html {
font-size: 16px; /* browser default */
}
.container {
width: 20rem; /* 20 × 16 = 320px */
padding: 1.5rem; /* 1.5 × 16 = 24px */
}
The core formula:
rem = px / root_font_size
px = rem × root_font_size
Simple, right? But there are traps.
Trap #1: Root Font Size Isn’t Always 16px#
Browsers default to 16px, but users can change this:
- Chrome: Settings → Font size
- Firefox: Options → Language and Appearance
- System-level: Windows Display settings
I’ve seen users with browser font size set to 20px. In that case, 1rem = 20px.
That’s why my converter supports custom base values:
function pxToRem(px: number, base: number = 16): string {
return (px / base).toFixed(4) + 'rem'
}
// Examples
pxToRem(24) // "1.5rem" (based on 16px)
pxToRem(24, 20) // "1.2rem" (based on 20px)
Why 4 decimal places? Precision matters—I’ll explain.
Trap #2: Precision Loss Causes Visual Drift#
Say you have a 750px-wide design mockup with a 120px button:
const designWidth = 750
const buttonWidth = 120
const rem = buttonWidth / designWidth * 10 // Assuming 1rem = 10px in mockup
console.log(rem) // 1.6rem
But what if the mockup is 375px wide?
const designWidth = 375
const buttonWidth = 60 // Half size
const rem = buttonWidth / designWidth * 10
console.log(rem) // 1.6rem
Same result, but different rendering:
- 750px mockup:
1.6rem × (screen width / 75) - 375px mockup:
1.6rem × (screen width / 37.5)
These formulas are common in mobile adaptation, but without understanding the principle, it’s easy to mess up.
My approach: define the base first, then calculate rem.
// Mobile adaptation setup
function setupMobileAdaptation(designWidth: number) {
const html = document.documentElement
const setFontSize = () => {
const screenWidth = html.clientWidth
// Design mockup: 10rem = design width
html.style.fontSize = (screenWidth / (designWidth / 10)) + 'px'
}
setFontSize()
window.addEventListener('resize', setFontSize)
}
// For 750px-wide mockup
setupMobileAdaptation(750)
// Now 1rem = screen width / 75
// 120px in mockup = 120 / 75 = 1.6rem
Trap #3: When to Use REM vs PX#
Not everything should use rem.
Good for REM#
- Font sizes: Scales with user preferences
- Spacing: padding, margin, gap—maintains proportion with text
- Container widths: max-width, width for responsive layouts
.article {
font-size: 1rem; /* Follows user preference */
line-height: 1.6; /* Unitless, relative to own font-size */
padding: 1.5rem; /* Proportional to text */
max-width: 40rem; /* ~640px, comfortable reading width */
}
Not Good for REM#
- Borders: 1px borders need special handling on retina displays
- Icon sizes: SVG icons usually have fixed dimensions
- Shadows: box-shadow blur radius typically stays fixed
.card {
border: 1px solid #e0e0e0; /* Fixed 1px */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Fixed values */
}
.icon {
width: 24px; /* Fixed size */
height: 24px;
}
Practical Tool: Batch Conversion#
When refactoring the legacy project, I wrote a conversion script:
function convertPxToRem(css, base = 16) {
// Match all px values, excluding border: 1px cases
return css.replace(/(\d+(?:\.\d+)?)px/g, (match, value) => {
const num = parseFloat(value)
// Keep 1px borders
if (num === 1) return match
// Convert others to rem
const rem = (num / base).toFixed(4)
return rem + 'rem'
})
}
// Test
const input = `
.container {
width: 1200px;
padding: 20px;
border: 1px solid #ccc;
}
`
console.log(convertPxToRem(input))
// Output:
// .container {
// width: 75rem;
// padding: 1.25rem;
// border: 1px solid #ccc;
// }
One problem: it doesn’t know which properties should keep px. I added a whitelist:
const pxWhitelist = [
'border', 'border-width', 'border-top-width',
'border-right-width', 'border-bottom-width', 'border-left-width',
'box-shadow', 'text-shadow'
]
function shouldKeepPx(property) {
return pxWhitelist.some(p => property.includes(p))
}
Quick Reference Table#
I find myself looking up common conversions frequently:
| PX | REM (16px base) |
|---|---|
| 8 | 0.5rem |
| 12 | 0.75rem |
| 16 | 1rem |
| 20 | 1.25rem |
| 24 | 1.5rem |
| 32 | 2rem |
| 48 | 3rem |
| 64 | 4rem |
This table is available in my PX/REM Converter—click to use, no manual calculation needed.
Summary#
Switching from px to rem means shifting from “fixed size” to “proportional size” thinking. Remember three things:
- Formula:
rem = px / base_font_size - Base: Default is 16px, but users can change it
- Use cases: Fonts and spacing use rem, borders and icons use px
Tool: PX/REM Converter
Related: CSS Gradient Generator | CSS Variable Generator