rem vs. em vs. px: Which Unit Should You Use?

Developers argue about units for a reason: the wrong choice forces rewrites, breaks zoom, and makes components feel brittle. By the end of this article you will know when to use rem, when em makes more sense, and when px remains fine. We will build a small, visual demo that compares the three units on the same UI: a simple product card with a ribbon label, an avatar circle, and a button. You will see how each unit reacts to scaling, and how that impacts readability and layout stability.

Why rem vs. em vs. px Matters

Units shape how your UI responds to user settings and component-level changes. rem sizes relative to the root, so increasing the browser’s base font size scales the entire interface predictably. em sizes relative to the current element, so nested components can scale in a more local, context-aware way. px is a physical pixel unit that stays fixed, which can help with crisp borders and iconography, but ignores user font size and can fight layout flexibility. Picking the right unit is not a style preference; it is about accessibility, maintainability, and predictable scaling across your system.

Prerequisites

You do not need a framework for this walkthrough. We will write plain HTML and CSS, with a few custom properties and pseudo-elements to draw the ribbon tail.

  • Basic HTML
  • CSS custom properties
  • CSS pseudo-elements (::before / ::after)

Step 1: The HTML Structure

The markup includes a wrapper with three cards: one styled with rem, one with em, and one with px. Each card has a badge-like ribbon, a circular avatar, a title, a description, and a button. The CSS will do most of the visual work, including the ribbon tail. The classes card-rem, card-em, and card-px keep the unit strategies isolated.


<div class="unit-demo">
  <article class="card card-rem">
    <span class="ribbon">REM</span>
    <div class="avatar" aria-hidden="true"></div>
    <h3 class="card__title">REM-based card</h3>
    <p class="card__text">This card sizes with the root. User zoom and OS font size changes flow through cleanly.</p>
    <button class="btn">Buy</button>
  </article>

  <article class="card card-em">
    <span class="ribbon">EM</span>
    <div class="avatar" aria-hidden="true"></div>
    <h3 class="card__title">EM-scaled card</h3>
    <p class="card__text">This card scales relative to its own font size. Nested parts compound that scale.</p>
    <button class="btn">Buy</button>
  </article>

  <article class="card card-px">
    <span class="ribbon">PX</span>
    <div class="avatar" aria-hidden="true"></div>
    <h3 class="card__title">PX-fixed card</h3>
    <p class="card__text">This card uses pixels for padding and elements. Text still respects user zoom, but spacing does not.</p>
    <button class="btn">Buy</button>
  </article>
</div>

Step 2: The Basic CSS & Styling

This stylesheet sets a sane default root size, a simple color palette, and a grid for the three cards. Spacing tokens use rem, because they should follow the root font size. The base card style sets position and layout, while each unit strategy refines sizing later.

/* CSS */
:root {
  /* Keep the browser default at 100% to respect user settings */
  font-size: 100%;

  --bg: #0f172a;          /* slate-900 */
  --panel: #111827;       /* gray-900 */
  --text: #e5e7eb;        /* gray-200 */
  --muted: #9ca3af;       /* gray-400 */
  --accent: #22d3ee;      /* cyan-400 */
  --accent-2: #fb7185;    /* rose-400 */
  --accent-3: #a78bfa;    /* violet-400 */

  /* Spacing scale in rem so it tracks root sizing */
  --space-1: 0.5rem;
  --space-2: 0.75rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2rem;
  --radius: 0.75rem;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
  line-height: 1.5;
}

.unit-demo {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--space-5);
  padding: var(--space-5);
  max-width: 1200px;
  margin-inline: auto;
}

.card {
  position: relative;
  background: var(--panel);
  border: 1px solid #1f2937; /* gray-800 */
  border-radius: var(--radius);
  padding: var(--space-4);
  overflow: hidden;
  box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset;
}

.card__title {
  margin: var(--space-3) 0 var(--space-2);
  font-weight: 700;
  font-size: 1.125rem;
}

.card__text {
  margin: 0 0 var(--space-4);
  color: var(--muted);
}

.btn {
  cursor: pointer;
  border: 0;
  color: #0c111b;
  background: var(--accent);
  font-weight: 700;
  padding: 0.625rem 1rem;
  border-radius: 0.5rem;
}

.ribbon {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  color: #0c111b;
  background: var(--accent);
  font-weight: 800;
  letter-spacing: 0.02em;
  padding: 0.25rem 0.75rem;
  border-radius: 0.375rem;
}

.avatar {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background: linear-gradient(135deg, #1e293b, #0b1220);
  border: 2px solid #1f2937;
}

Advanced Tip: Keep spacing tokens in rem at the system level. It gives you one dial (the root font size) that can scale paddings, gaps, and radii across the board without touching component code.

Step 3: Building the REM-based Card

Here we tailor the REM card so that both its avatar size and ribbon tail use rem. This makes the component track the root exactly. If the user sets a larger base font size for readability, the card’s proportions remain intact.

/* CSS */
.card-rem {
  /* No custom font-size; inherits 1rem from the root scale */
}

.card-rem .avatar {
  width: 3rem;
  height: 3rem;
}

.card-rem .ribbon {
  background: var(--accent);
}

/* Right-pointing triangle tail built with borders in rem */
.card-rem .ribbon::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -0.75rem;
  transform: translateY(-50%);
  border-left: 0.75rem solid var(--accent);
  border-top: 0.5rem solid transparent;
  border-bottom: 0.5rem solid transparent;
}

How This Works (Code Breakdown)

On the REM card, font sizes and spacing inherit directly from the root. The avatar uses width and height in rem, so changing the root affects it equally across the system. The ribbon tail uses the classic border triangle technique with rem units. If you have not built one before, this is the same approach used in a triangle-right with CSS, only attached to a label.

Using rem here makes the card predictable under zoom. If a user bumps the browser font size from 16px to 20px, 1rem becomes 20px and the avatar grows from 48px to 60px, the padding grows with it, and the triangle tail keeps its proportions. This is a strong default for general UI spacing. If you plan to design more decorative labels, the full ribbon banner technique builds on the same triangle trick.

Step 4: Building the EM and PX Cards

The EM card scales from its own font size, which means nested elements compounded in em will scale together if the card font size changes. The PX card uses fixed spacing for a contrast: text still responds to user zoom, but the avatar, padding, and ribbon tail stay fixed in device pixels.

/* CSS */
/* EM strategy: component font-size drives the rest */
.card-em {
  font-size: 1rem; /* start from root, but we will scale locally on hover later */
}

.card-em .avatar {
  width: 3em;
  height: 3em;
}

.card-em .ribbon {
  background: var(--accent-2);
  padding: 0.25em 0.75em;
}

.card-em .ribbon::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -0.75em;
  transform: translateY(-50%);
  border-left: 0.75em solid var(--accent-2);
  border-top: 0.5em solid transparent;
  border-bottom: 0.5em solid transparent;
}

/* PX strategy: fixed spacing and elements */
.card-px {
  /* No change to font-size; let text respond to browser zoom */
  padding: 24px; /* pixels ignore root scaling */
}

.card-px .avatar {
  width: 48px;
  height: 48px;
}

.card-px .ribbon {
  background: var(--accent-3);
  padding: 4px 12px;
}

.card-px .ribbon::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -12px;
  transform: translateY(-50%);
  border-left: 12px solid var(--accent-3);
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
}

How This Works (Code Breakdown)

On the EM card, everything is attached to the component’s own font size. The avatar is 3em, the ribbon padding uses em, and the triangle tail uses em. If you raise .card-em { font-size } later, all those parts grow in sync without touching any child rules. This is valuable when the component appears inside a scalable container such as a compact sidebar vs. a spacious content area.

The PX card uses pixels for spacing and the avatar. That can keep borders crisp and avoid rounding differences across browsers. The tradeoff is that spacing no longer follows root scaling. When a user increases the base font size, the text grows but the padding, avatar, and triangle tail do not, which can produce cramped layouts. Use px when you need hard edges or guaranteed consistency for hairlines, but avoid it for global spacing systems.

The circular avatar uses border-radius: 50%. If you want a refresher on shape-only techniques, the approach mirrors the method in how to make a circle with CSS, which pairs width and height, then rounds it perfectly with a 50% radius.

Advanced Techniques: Adding Animations & Hover Effects

We will add subtle hover effects to show how em compounding feels in practice. The REM card will shift up and add a shadow, while the EM card will raise its own font size so its em-based parts scale together. Animations should be light and respect user preferences.

/* CSS */
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 160ms ease, box-shadow 160ms ease;
  }
  .card:hover {
    transform: translateY(-2px);
    box-shadow: 0 12px 28px rgba(0,0,0,0.35);
  }
}

/* Show EM compounding: scale the component font size on hover */
@media (prefers-reduced-motion: no-preference) {
  .card-em {
    transition: transform 160ms ease, box-shadow 160ms ease, font-size 160ms ease;
  }
  .card-em:hover {
    font-size: 1.125rem; /* everything in em grows together */
  }
}

/* Buttons: slight press/hover styles */
.btn {
  transition: background-color 120ms ease, transform 120ms ease;
}

.btn:hover {
  transform: translateY(-1px);
}

.btn:active {
  transform: translateY(0);
}

When you hover the EM card, its font-size rises to 1.125rem, so the avatar (3em), the ribbon padding, and the triangle tail all scale in step. The REM card ignores that local change, because rem values ignore the element’s own font size, which is the point. This makes REM a safe bet for system-level spacing, and EM a tool for component-level scaling.

Accessibility & Performance

The unit you choose alters how your UI reacts to user settings and how much work your layout engine needs to do during resizes and zoom. Get these details right early and you avoid brittle behavior later.

Accessibility

Favor rem for typography and spacing that should respect user font size presets. Users who increase the default font size rely on sites that scale predictably. Avoid setting html { font-size: 62.5% } just to think in tens. That pattern reduces effective size for users and adds friction for assistive tech. If a component needs to grow or shrink in a single area, em gives you scoped control without fighting the rest of the system.

Decorative elements should not be announced. The avatar in this demo is decoration, so aria-hidden=”true” is sufficient. If your ribbon conveys a real status, set an accessible name on the card or include screen-reader-only text. For motion, the prefers-reduced-motion query prevents unnecessary animation for those who opt out of movement.

Performance

rem, em, and px are all cheap to render. The tradeoffs are about predictable layout changes. rem keeps component spacing stable across nested contexts; em can compound changes, which is powerful but can surprise you if a parent adjusts font-size and doubles a child’s scale. px yields the fewest surprises during layout but ignores root-based scaling, which can degrade the reading experience. Animations in this article touch transform and opacity-like effects that stay on the compositor, which keeps frames smooth.

Warning: em compounding surprises teams. If a parent sets font-size: 1.25em and a child uses padding: 2em, a later change to the parent doubles the child padding. Audit em chains in complex layouts and prefer rem for shared spacing tokens.

The last closing paragraph

You built three versions of the same UI and saw how rem tracks the root, em follows the component, and px stays fixed. You also learned where a border-based triangle fits in a ribbon label and how subtle motion highlights em-based scaling. Now you can choose the right unit for each job and ship components that read well, scale cleanly, and stay predictable in every context.

Leave a Comment