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#

  1. Font sizes: Scales with user preferences
  2. Spacing: padding, margin, gap—maintains proportion with text
  3. 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#

  1. Borders: 1px borders need special handling on retina displays
  2. Icon sizes: SVG icons usually have fixed dimensions
  3. 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:

  1. Formula: rem = px / base_font_size
  2. Base: Default is 16px, but users can change it
  3. Use cases: Fonts and spacing use rem, borders and icons use px

Tool: PX/REM Converter


Related: CSS Gradient Generator | CSS Variable Generator