Skip to content

Viewport Units and Fluid Typography

beginner9 min read

vh Is Broken on Mobile -- And the Fix Changes Everything

100vh should mean "full viewport height." On desktop, it does. On mobile? It includes the area behind the browser's URL bar and bottom navigation. When the URL bar is visible, 100vh extends below the visible screen. Users see scrollbars on content that should fill the screen exactly. This bug (really a spec issue) persisted for years before the new viewport unit variants finally solved it.

Mental Model

Think of mobile browser viewport units as three measuring tapes. lvh (large) measures the viewport with the URL bar hidden — the maximum visible area. svh (small) measures with the URL bar visible — the minimum visible area. dvh (dynamic) reads the current state and updates live as the URL bar slides in and out. For most use cases, dvh is what you actually want — it matches what the user can see right now.

The Viewport Unit Variants

UnitFull NameDescription
vh / vwViewport height/widthThe original — problematic on mobile
svh / svwSmall viewportURL bar visible (smallest viewport)
lvh / lvwLarge viewportURL bar hidden (largest viewport)
dvh / dvwDynamic viewportAdapts as browser chrome appears/disappears
vmin / vmaxMin/max of vh and vwResponsive to orientation changes
/* Hero section — actually fills the visible screen on mobile */
.hero {
  min-height: 100dvh;
}

/* Alternative: svh for guaranteed no content cutoff */
.hero {
  min-height: 100svh;
  /* Content always visible, even with URL bar showing */
}
Common Trap

dvh causes layout reflow as the URL bar animates in and out. For elements that should not resize during scroll (like a fixed header background), use svh or lvh instead. dvh is best for hero sections and overlays where you want the element to match the visible area at all times.

When to Use Each

Use CaseUnitWhy
Hero sections100dvhMatches visible area as it changes
Fixed overlays/modals100dvh or 100svhNever extends behind browser chrome
Background images100lvhAvoids reflow during scroll
Sticky elements height calc100svhSafe minimum for visible calculations

Fluid Typography System

This is one of the most impactful things you can learn in modern CSS. No more juggling font-size media queries.

The clamp() Formula

h1 {
  font-size: clamp(minimum, preferred, maximum);
  font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem);
}

How it works:

  • minimum (1.5rem): Never smaller than this — protects readability
  • preferred (4vw + 0.5rem): The fluid value — scales with viewport
  • maximum (3rem): Never larger than this — prevents absurdly large text

The + 0.5rem in the preferred value is important: it sets the "base" so the text doesn't become too small on narrow viewports before hitting the minimum.

Complete Fluid Type Scale

:root {
  --step--2: clamp(0.69rem, 0.66rem + 0.18vw, 0.80rem);
  --step--1: clamp(0.83rem, 0.78rem + 0.29vw, 1.00rem);
  --step-0:  clamp(1.00rem, 0.91rem + 0.43vw, 1.25rem);
  --step-1:  clamp(1.20rem, 1.07rem + 0.63vw, 1.56rem);
  --step-2:  clamp(1.44rem, 1.26rem + 0.89vw, 1.95rem);
  --step-3:  clamp(1.73rem, 1.48rem + 1.24vw, 2.44rem);
  --step-4:  clamp(2.07rem, 1.73rem + 1.70vw, 3.05rem);
  --step-5:  clamp(2.49rem, 2.03rem + 2.31vw, 3.82rem);
}

body { font-size: var(--step-0); }
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-3); }
h4 { font-size: var(--step-2); }
small, .caption { font-size: var(--step--1); }

Calculating the Preferred Value

The formula to create a fluid value between two sizes across a viewport range:

preferred = minimum + (maximum - minimum) * ((viewport - viewportMin) / (viewportMax - viewportMin))

In practice, use a tool like utopia.fyi to generate the clamp() values. But understanding the math:

/* Goal: 1.5rem at 320px viewport, 3rem at 1200px viewport */
/* Rate = (3 - 1.5) / (1200 - 320) = 1.5 / 880 = 0.17% per viewport px */
/* In vw: 0.17 * 100 = 17.05vw... that's too aggressive */
/* Better: mix rem and vw */
h1 { font-size: clamp(1.5rem, 1rem + 2.27vw, 3rem); }

Fluid Spacing

Why stop at fonts? Apply the same clamp() approach to spacing:

:root {
  --space-xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.5rem);
  --space-sm: clamp(0.5rem, 0.4rem + 0.5vw, 1rem);
  --space-md: clamp(1rem, 0.8rem + 1vw, 2rem);
  --space-lg: clamp(1.5rem, 1rem + 2.5vw, 4rem);
  --space-xl: clamp(2rem, 1rem + 5vw, 6rem);
}

.section { padding: var(--space-lg) var(--space-md); }
.card { gap: var(--space-sm); }
Quiz
You use min-height: 100dvh on a fixed hero background. Users report the hero resizes jerkily on mobile scroll. Why?
Why mixing rem and vw matters in clamp()

Using 4vw alone as the preferred value means at 320px viewport, the font is 12.8px — too small. At 1920px, it's 76.8px — absurdly large. Adding a rem component (4vw + 0.5rem) shifts the baseline up so the scaling starts from a reasonable size. The rem part provides the floor, the vw part provides the responsiveness.

Execution Trace
320px viewport
clamp(1.5rem, 1rem + 2.27vw, 3rem)
1rem + 2.27vw = 16px + 7.26px = 23.26px = 1.45rem → clamped to 1.5rem (minimum)
768px viewport
1rem + 2.27vw = 16px + 17.45px
= 33.45px = 2.09rem (within range, no clamping)
1200px viewport
1rem + 2.27vw = 16px + 27.24px
= 43.24px = 2.70rem (within range, no clamping)
1920px viewport
1rem + 2.27vw = 16px + 43.58px
= 59.58px = 3.72rem → clamped to 3rem (maximum)
What developers doWhat they should do
Using 100vh for full-screen elements on mobile
100vh includes the area behind the mobile URL bar — content gets cut off
Use 100dvh (dynamic) or 100svh (small) for mobile-safe viewport height
Using only vw for fluid font-size (like font-size: 4vw)
Pure vw has no minimum or maximum — text becomes unreadable on small screens and absurd on large ones
Use clamp() with a rem minimum, vw+rem preferred, and rem maximum
Forgetting to add a rem component to the preferred clamp value
Without the rem addition, the font size starts from 0 at 0vw, making it too small on small viewports
Mix rem + vw: clamp(1.5rem, 1rem + 2vw, 3rem) — rem provides the baseline
Using dvh for fixed backgrounds (causes reflow on mobile scroll)
dvh updates continuously as the URL bar slides — causing layout reflow on every scroll
Use lvh for backgrounds and elements that shouldn't resize during URL bar animation
Quiz
On a mobile browser with the URL bar visible, which unit matches the actual visible screen height?
Quiz
What does the rem component do in clamp(1.5rem, 1rem + 2vw, 3rem)?
Key Rules
  1. 1Use dvh for elements that should match the visible viewport height on mobile (heroes, modals)
  2. 2Use svh when you want the safe minimum viewport height (URL bar visible)
  3. 3Use lvh for backgrounds to avoid reflow during URL bar animation
  4. 4Build fluid typography with clamp(rem-min, rem + vw, rem-max) — never pure vw
  5. 5Apply the same fluid approach to spacing, padding, and gap with clamp()