Skip to content

Colors, Units, and Values

beginner10 min read

The Unit You Choose Changes the User Experience

16px and 1rem both produce the same font size on most screens. Sounds the same, right? But when a visually impaired user sets their browser's base font size to 24px, 16px stays at 16px while 1rem scales to 24px. That single unit choice is the difference between an accessible site and one that forces users to zoom.

Units and values aren't a syntax preference. They're a design decision with real consequences for layout, accessibility, and responsiveness.

Mental Model

Think of CSS units as currencies with different exchange rates. px is a fixed denomination — 16px is always 16px regardless of context. rem is pegged to the root exchange rate — if the root changes, all rem values shift proportionally. em is pegged to the local exchange rate — each element's font-size becomes the new base. % depends on what you're buying — width percentages reference the parent's width, but padding percentages reference the parent's width too (not height).

Absolute vs Relative Units

Absolute Units

.box {
  width: 300px;     /* Pixels — fixed size, 1px = 1/96th of an inch */
  font-size: 12pt;  /* Points — 1pt = 1/72nd of an inch (print) */
}

In practice, px is the only absolute unit used on the web. Others (cm, mm, in, pt, pc) are for print stylesheets.

Relative Units — Font-Based

:root {
  font-size: 16px; /* Base for rem calculations */
}

.parent {
  font-size: 20px;
}

.child {
  font-size: 1.5rem;  /* 24px (1.5 × 16px root) */
  font-size: 1.5em;   /* 30px (1.5 × 20px parent) */
  padding: 1em;       /* 30px if font-size is 1.5em (relative to own computed font-size) */
  margin: 1rem;       /* 16px (always relative to root) */
}

When to use each:

UnitBest ForWhy
remFont sizes, spacing, component sizingScales with user preferences, predictable
emSpacing relative to text size (padding in buttons, icon sizing)Scales proportionally with the element's own font size
pxBorders, shadows, very small fixed valuesWhen you need pixel-precise control
Common Trap

em for font-size means "relative to the parent's font-size." But em for padding, margin, and width means "relative to the element's own computed font-size." This dual behavior catches everyone at least once. If you set font-size: 2em and padding: 1em on the same element, the padding is relative to the doubled font-size, not the parent's.

Relative Units — Viewport-Based

.hero {
  height: 100vh;   /* 100% of viewport height */
  width: 100vw;    /* 100% of viewport width */
  padding: 5vmin;  /* 5% of the smaller viewport dimension */
}

.title {
  font-size: 5vw;  /* Scales with viewport width */
}

The mobile vh problem: On mobile browsers, 100vh includes the area behind the URL bar. When the URL bar is visible, content at 100vh extends below the visible screen.

/* Modern fix — dynamic viewport units */
.hero {
  height: 100dvh; /* Dynamic: adjusts as URL bar shows/hides */
}

/* Other options: */
.a { height: 100svh; } /* Small viewport: URL bar visible */
.b { height: 100lvh; } /* Large viewport: URL bar hidden */

Percentage Gotchas

.child {
  width: 50%;        /* 50% of parent's content width */
  height: 50%;       /* 50% of parent's explicit height (or nothing!) */
  padding: 10%;      /* 10% of PARENT'S WIDTH (not height!) */
  margin: 10%;       /* 10% of PARENT'S WIDTH (even vertical margin) */
  transform: translateX(50%); /* 50% of the element's OWN width */
}
Why padding percentages reference width, not height

Vertical padding as a percentage of the parent's width was a deliberate design choice. It prevents circular dependencies: if vertical padding depended on height, and height depended on content (which includes padding), you'd have an infinite loop. Referencing width breaks the circularity. This behavior is also useful — it enables the "padding-top aspect ratio" hack (before aspect-ratio existed).

The clamp() Function -- Fluid Responsive Values

If you take one thing from this section, let it be clamp(). It creates values that scale fluidly between bounds:

.container {
  /* Fluid width: at least 320px, prefer 80%, cap at 1200px */
  width: clamp(320px, 80%, 1200px);

  /* Fluid padding: at least 1rem, scale with viewport, cap at 4rem */
  padding: clamp(1rem, 3vw, 4rem);
}

h1 {
  /* Fluid typography: minimum 1.5rem, scale with viewport, max 3rem */
  font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem);
}

This replaces media queries for many responsive scenarios.

Modern CSS Color

The Problem with rgb/hsl

You'd think two colors with the same lightness value would look equally bright. They don't. rgb() and hsl() use the sRGB color space, which has a key flaw: perceptual non-uniformity. Two colors with the same lightness value in HSL don't look equally bright to the human eye. Yellow at hsl(60, 100%, 50%) appears much brighter than blue at hsl(240, 100%, 50%), despite identical lightness values.

oklch -- Perceptually Uniform Color

oklch() fixes this entirely. Colors with the same lightness value actually look equally bright:

.card-blue {
  /* oklch(lightness chroma hue) */
  background: oklch(0.7 0.15 240);  /* 70% lightness, blue */
}

.card-green {
  background: oklch(0.7 0.15 150);  /* 70% lightness, green */
  /* Visually same brightness as the blue — impossible with hsl */
}

oklch components:

  • Lightness (L): 0 (black) to 1 (white) — perceptually uniform
  • Chroma (C): 0 (gray) to ~0.4 (most vivid) — saturation that actually works
  • Hue (H): 0-360 degrees — the color wheel

Practical Color Palette with oklch

:root {
  /* Generate a palette by only changing lightness */
  --blue-50: oklch(0.97 0.02 240);
  --blue-100: oklch(0.93 0.04 240);
  --blue-200: oklch(0.87 0.08 240);
  --blue-300: oklch(0.78 0.12 240);
  --blue-400: oklch(0.68 0.16 240);
  --blue-500: oklch(0.58 0.19 240);
  --blue-600: oklch(0.48 0.19 240);
  --blue-700: oklch(0.38 0.16 240);
  --blue-800: oklch(0.30 0.12 240);
  --blue-900: oklch(0.22 0.08 240);
}

The advantage: consistent perceived brightness across hues. Every 500-level color in your palette will look the same brightness.

Execution Trace
Root
html { font-size: 16px }
Base for all rem calculations
Parent
.parent { font-size: 1.25rem }
Computed: 20px (1.25 × 16)
Child font
.child { font-size: 1.5em }
Computed: 30px (1.5 × 20 parent)
Child padding
.child { padding: 1em }
Computed: 30px (1 × 30 own font-size)
Child margin
.child { margin: 1rem }
Computed: 16px (1 × 16 root)

Production Scenario: Accessible Font Scaling

A user sets their browser default font size to 24px (150% of the default 16px):

/* Bad: Fixed pixels — ignores user preference */
body { font-size: 16px; }
h1 { font-size: 32px; }
/* User still sees 16px and 32px */

/* Good: Relative units — respects user preference */
body { font-size: 1rem; }  /* Now 24px for this user */
h1 { font-size: 2rem; }    /* Now 48px for this user */

/* Best: Fluid with clamp and rem */
h1 { font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem); }
/* Scales fluidly AND respects user preference */
What developers doWhat they should do
Using px for font-size throughout the project
Users who increase their browser font-size (for accessibility) get no benefit from px-based font sizes
Use rem for font sizes so they scale with user's browser preference
Using 100vh for full-screen sections on mobile
100vh includes the area behind the mobile browser's URL bar, causing content to be cut off
Use 100dvh (dynamic viewport height) or 100svh (small viewport height)
Expecting padding-top: 50% to be 50% of the parent's height
CSS spec design avoids circular height dependencies
Vertical padding/margin percentages always reference the parent's width
Using hsl() and expecting perceptually uniform lightness across hues
HSL lightness is mathematically uniform but not perceptually uniform
Use oklch() for perceptually uniform color — same lightness values actually look equally bright
Quiz
A user sets their browser font-size to 20px. What does h1 { font-size: 2rem; } compute to?
Quiz
An element has font-size: 2em and padding: 1em. The parent's font-size is 16px. What is the padding?
Quiz
Which oklch value produces the most saturated color?
Key Rules
  1. 1Use rem for font sizes and spacing to respect user accessibility preferences
  2. 2Use em only for values that should scale relative to the element's own font-size (e.g., button padding)
  3. 3Use px for borders, shadows, and small fixed values where scaling would be meaningless
  4. 4Use dvh/svh/lvh instead of vh on mobile to avoid the URL bar problem
  5. 5Prefer oklch() for perceptually uniform color palettes — same lightness values look equally bright across hues