Units & Math Functions
Master absolute vs relative units (px, rem, em, vw, dvh) and modern CSS math functions: calc(), min(), max(), and clamp().
🎯 What You Will Master in This Lesson
px, rem, em, %, vw/vh/dvh, ch, vmin/vmax — when to use each.calc() Anatomy: Mixing mismatched units, operator rules, and nesting.min() & max(): Single-line replacements for old two-property hacks.clamp() Fluid Typography: True responsive text without a single media query.dvh vs svh vs lvh — solving the iOS address bar bug.📏 The Complete CSS Unit Cheat Sheet
CSS has two families of units: absolute (fixed, never change) and relative (scale based on something else). Knowing which family to use — and which reference point — is one of the highest-leverage CSS skills.
ABSOLUTEFixed — ignore user preferences and parent size
px (Pixels)One CSS pixel. The most precise absolute unit. Best for: 1px hairline borders, box-shadow offsets, icon sizes. Avoid for: font-size (breaks browser zoom accessibility).
pt, cm, mm, in (Print units)Physical real-world units meant for print stylesheets. 1in = 96px on screen. Almost never used in web CSS — only in @media print rules.
RELATIVEFlexible — scale based on a reference point
rem (Root EM)Relative to <html> font-size1rem = 16px (browser default). Respects user browser zoom. Industry standard for all typography and spacing. Consistent across nested components.
emRelative to PARENT element font-sizeCompounds in nested elements! 1em inside a 2em parent = 32px. Best for: letter-spacing, line-height, border-radius relative to text size.
% (Percentage)Relative to PARENT element dimensionwidth: 50% = half of parent container width. For height: %, the parent must have an explicit height set. Most fluid layout use-case.
vw / vhRelative to VIEWPORT (browser window)1vw = 1% of viewport width. 100vh = full browser window height. Problem: vh ignores mobile address bar.
dvh / svh / lvhDynamic/Small/Large Viewport Heightdvh = dynamic, updates as mobile address bar shows/hides. svh = smallest (bar visible). lvh = largest (bar hidden). Use 100dvh for full-screen mobile layouts.
chRelative to current font's "0" glyph width1ch ≈ width of the “0” character. Typography research: 45–75 chars/line is optimal. max-width: 65ch on article text = perfect reading comfort on any screen, any font.
⚠️ The #1 Confusing Pair: rem vs em
Both sound similar. Both are “relative EM units.” But they reference completely different things — and confusing them creates compounding (cascading multiplication) bugs that are very hard to debug:
/* rem — always relative to <html> = 16px */
html { font-size: 16px; }
.parent { font-size: 2rem; } /* = 32px */
.child { font-size: 1rem; } /* = 16px ✅ always */
/* em — relative to PARENT element font-size */
html { font-size: 16px; }
.parent { font-size: 2em; } /* = 32px */
.child { font-size: 1em; } /* = 32px! ⚠️ inherits parent */
Rule of thumb: Use rem for font sizes and spacing (always predictable). Use em only for properties that should scale relative to that element's own font size — like letter-spacing: 0.05em or padding: 0.75em on a button.
🏔️ Three Measuring Tools — The Iron Ruler, Tailor's Tape, and Mountain Guardrails
The Iron Ruler (px)
A cold steel 30cm ruler. Never bends, never stretches. If you use it to tailor clothes for a growing child, the seams will burst. In CSS, hardcoding px on font sizes breaks accessibility for users who zoom their browser.
Use px for: borders, shadows, icons. Avoid for: font-size.
The Tailor's Elastic Tape (rem & vw)
It breathes with the body. When a user zooms their browser font from 16px to 24px, all rem measurements expand in harmony. When the browser window widens, vw expands proportionally.
The industry standard for typography and spacing.
The Mountain Highway Guardrails (clamp())
On a winding cliff road you want to drive freely, but you need guardrails so you don't fall off. clamp(min, preferred, max) lets font size scale with viewport width while guaranteeing it never becomes illegibly small on mobile or grotesquely huge on 4K.
Fluid design with hard safety limits.
🧮 calc() — Mixing Units That Normally Can't Mix
calc() solves the fundamental CSS problem: you cannot normally subtract px from % because they resolve at different times. calc() defers the math to render time when both values are known.
The 4 Operators — and the Critical Whitespace Rule
+ (addition) & * (multiplication)Whitespace optional around + technically, but always write spaces for consistency. Whitespace optional around *.
- (subtraction) — MANDATORY whitespace!CSS sees -40px as a negative number literal. Without space, calc(100%-40px) silently fails! This is the #1 calc() bug.
Nesting calc() inside calc()You can nest multiple calc() calls for complex formulas:
But modern CSS allows inline math inside min(), max(), clamp() without wrapping in calc().
Real use-cases/* sidebar + content */
width: calc(100% - 280px);
/* scrollbar offset */
width: calc(100vw - 17px);
/* stacked nav + content */
height: calc(100dvh - 64px);
↔️ min() and max() — Smart Single-Line Constraints
min(a, b) — picks the smaller value
Replaces the classic max-width + width two-property pattern. The browser evaluates both values and uses whichever is numerically smaller.
/* Old pattern — 2 properties */
.card { max-width: 600px; width: 90%; }
/* Modern — 1 property */
.card { width: min(600px, 90%); }
On desktop: 600px < 90% of 1440px (1296px) → uses 600px. On mobile 390px: 90% of 390px (351px) < 600px → uses 351px. Perfect mobile margins automatically!
max(a, b) — picks the larger value
Guarantees a minimum value. Replaces min-width combos. Great for ensuring touch targets meet the minimum 44px WCAG accessibility requirement.
/* Guaranteed minimum touch target */
.btn { padding: max(12px, 3vw); }
/* Fluid padding with a floor */
.section { padding: max(24px, 8vw); }
On mobile 390px: max(12px, 3% of 390 = 11.7px) → uses 12px. On desktop 1440px: max(12px, 3% of 1440 = 43.2px) → uses 43.2px. Auto-scaling with a safe minimum!
📎 clamp() — Fluid Typography Without Media Queries
clamp(minimum, preferred, maximum) is max(minimum, min(preferred, maximum)) — it clamps a fluid value between a hard lower and upper bound. Think of it as a slider with stops at both ends.
Visualizing clamp(1rem, 4vw, 2.5rem)
4vw = 16pxPreferred ≤ Minimum → browser uses 1rem (16px) — clamped at bottom.
4vw = 24pxPreferred is between bounds → browser uses 24px — fluid scaling!
4vw = 40px+Preferred ≥ Maximum → browser uses 2.5rem (40px) — clamped at top.
/* The fluid typography formula — works from 320px to 4K monitors */
h1 { font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem); }
p { font-size: clamp(1rem, 1.5vw + 0.5rem, 1.25rem); }
.section { padding: clamp(1.5rem, 5vw, 5rem); }
/* The + 0.5rem inside preferred prevents font from hitting 0 on microscopic screens */
📱 Mobile Viewport Units — Solving the iOS Address Bar Bug
100vh seemed perfect for full-screen mobile layouts — until iOS Safari and mobile Chrome made the address bar slide up and down during scrolling. 100vh ignores this, so the bottom 50–80px of your page gets hidden behind the bar.
100vhOld behavior (broken). Calculated once when page loads. Ignores address bar height changes. Bottom CTA buttons hidden on iOS Safari.
100dvhDynamic. Updates in real-time as the address bar expands and collapses during scroll. Use this for full-screen app shells and hero sections.
svh / lvhSmall / Large viewport. svh = height with bar fully visible. lvh = height with bar hidden. Use when you need a stable non-jumping value.
/* Hero section — use dvh for correct mobile full-screen */
.hero { min-height: 100dvh; }
/* Safe fallback for older browsers: */
.hero { min-height: 100vh; min-height: 100dvh; }
💻 Live Fluid Units & clamp() Playground
The card uses min(520px, 92%) for width, clamp() for fluid heading and body text, and calc(100% - 100px) for the search input. Try resizing the preview, changing the clamp values, or adding max-width: 65ch to the paragraph!
Line-by-Line Code Breakdown
The key patterns inside the playground — explained in full:
width: min(520px, 92%);Replaces the classic two-line combo of max-width: 520px; width: 92%;. The browser evaluates both at render time: on a 1440px desktop, 92% = 1324.8px > 520px, so it picks 520px. On a 390px iPhone, 92% = 358.8px < 520px, so it picks 358.8px — leaving 4% breathing room on each side automatically.
font-size: clamp(1.25rem, 4vw + 0.5rem, 2.25rem);The fluid typography sweet spot. The preferred value is 4vw + 0.5rem — a combination of viewport-relative and root-relative. The + 0.5rem ensures the font has a baseline size even on microscopic screens (like smartwatches at 150px width, where pure 4vw = 6px would be illegible). The clamp() then ensures it never goes below 1.25rem (20px) or above 2.25rem (36px).
width: calc(100% - 100px);The mandatory spaces around -. This makes the input box fill all horizontal space except the 100px submit button sitting beside it. Without calc(), you couldn't subtract a fixed pixel value from a flexible percentage — they resolve at different render stages. The spaces around the minus sign are not stylistic: without them, CSS reads 100%-100px as invalid syntax and silently ignores the entire rule.
max-width: 65ch;The typography readability unit. 1ch equals the width of the 0 glyph in the current font. Research shows 45–75 characters per line is the optimal reading comfort zone. Setting max-width: 65ch on body text ensures the line length is ideal on any screen and any font — no media query needed. The value auto-adjusts if the user changes their font size preference.
Q1Why does the CSS expression `width: calc(100%-40px);` fail with a syntax error, while `width: calc(100% - 40px);` works perfectly?
Q2What does `font-size: clamp(1rem, 2.5vw, 2rem);` do?
Q3Why was the new `100dvh` (Dynamic Viewport Height) unit introduced to replace `100vh` on mobile phones?
Practice Challenge: Build a Full Fluid System
Open the live editor above and try these 4 experiments in order:
- Fluid padding: Find
.fluid-card. Change itspaddingtoclamp(12px, 5vw, 40px);. Watch how the card's breathing room scales smoothly as you resize the preview! - Layout balance: Set
.action-btnwidth to120px. Then set.main-bartowidth: calc(100% - 120px);. The input now fills exactly the remaining space regardless of container width. - Typography guard: Find the
<p>tag paragraph. Addmax-width: 65ch;to its selector. The paragraph will automatically stop at a comfortable reading width on wide screens — no media query needed! - Bonus — Hero section: Add a new
<div class="hero">. Give itmin-height: 100dvh;and a dark background. Notice the card no longer gets cut off — the viewport height respects the browser chrome!
💡 Click to view the full solution code
.fluid-card { width: min(520px, 92%); padding: clamp(12px, 5vw, 40px); }
.main-bar { width: calc(100% - 120px); }
.action-btn { width: 120px; }
p { max-width: 65ch; }
.hero { min-height: 100dvh; background: #0f172a; }
Common Pitfalls & Interview Preparation
❌ Pitfall 1: Missing Whitespace in `calc()`
calc(100%-20px) silently breaks in all browsers with no error in the console. The rule is not optional: spaces around + and - are required by the CSS spec, not a style preference. Always: calc(100% - 20px).
❌ Pitfall 2: Using `px` for Font Sizes
Setting font-size: 16px on body or headings overrides the user's browser default zoom preference. Users with low vision who set their browser to 200% zoom expect text to double. Use rem for font sizes — it respects user zoom settings and passes WCAG 1.4.4 accessibility checks.
✅ Best Practice: Math Inside min/max/clamp Without calc()
Modern CSS (Level 4) allows arithmetic expressions directly inside min(), max(), and clamp() without wrapping in calc(). So clamp(16px, 2vw + 10px, 32px) is valid and preferred over the verbose clamp(16px, calc(2vw + 10px), 32px).
✅ Best Practice: Always Provide `100vh` Fallback
dvh is well-supported (Chrome 108+, Safari 15.4+, Firefox 101+) but older browsers don't know it. Always declare the safe fallback first: min-height: 100vh; min-height: 100dvh;. Browsers ignore unknown values, so old browsers use vh, modern ones use dvh.
💬 Interview Q: What is the `ch` unit and why is it beloved in typography?
1ch equals the width of the zero (0) glyph in the current active font. It's a proxy for “one character width” in the current typeface.
Typography research (Baymard Institute, Nielsen Norman) consistently shows that 45–75 characters per line produces the optimal reading comfort. Fewer than 45 = too many eye jumps. More than 75 = hard to track back to line start.
max-width: 65ch on article body text guarantees perfect reading line length regardless of font size, font family, or screen size. The ch value scales automatically when users change font size — something a px-based max-width cannot do.
💬 Interview Q: What is the difference between `svh`, `lvh`, and `dvh`?
All three represent viewport height on mobile but measure different states of the browser chrome:
- svh (Small Viewport Height): The viewport height when the browser's UI bars (address bar, bottom nav) are fully expanded and visible. This is the minimum visible height — the most conservative.
- lvh (Large Viewport Height): The viewport height when the browser's UI bars are fully retracted/hidden (after scrolling down). This is the maximum possible height.
- dvh (Dynamic Viewport Height): Updates in real-time as the user scrolls and the bars show/hide. Most accurate, but can cause layout reflows if used on animated elements. Best for hero sections.
Practical advice: Use 100dvh for hero/app shells. Use svh when you want guaranteed no-clipping at worst case.
💬 Interview Q: What's the difference between `rem` and `em`, and when would you use `em`?
rem (Root EM) is always relative to the <html> element's font-size (default 16px). It's consistent everywhere and doesn't compound in nested elements.
em is relative to the current element's own font-size (or inherited font-size from parent). In nested elements, em values multiply: a 2em parent with a 1.5em child = 3em total size relative to root!
When to use em: Properties that should scale proportionally to the element's own text size — like letter-spacing: 0.05em, line-height: 1.6 (unitless preferred), or padding: 0.75em 1.25em on buttons where padding should grow/shrink with the button's font size.
💬 Interview Q: How would you create a fluid type scale without media queries?
Use clamp() with a viewport-relative preferred value mixed with a root-relative base. The formula clamp(min, preferred-vw + base-rem, max) scales smoothly across all screen sizes:
/* Complete fluid type scale — no media queries needed */
:root {
--fs-sm: clamp(0.875rem, 1vw + 0.5rem, 1rem);
--fs-base: clamp(1rem, 1.5vw + 0.5rem, 1.25rem);
--fs-lg: clamp(1.25rem, 2vw + 0.5rem, 1.75rem);
--fs-xl: clamp(1.5rem, 3vw + 0.5rem, 2.5rem);
--fs-2xl: clamp(2rem, 4vw + 0.5rem, 3.5rem);
}
This approach eliminates brittle breakpoint-based font-size overrides and produces text that looks correct on every screen size, including sizes in between standard breakpoints.