CSS Viewport Units Explained

How vw, vh, vmin and vmax work, svh vs dvh vs lvh for the mobile 100vh bug, and the 100vw scrollbar fix.

Viewport units are CSS units whose values are proportional to the size of the browser window (the viewport). This guide covers how vw, vh, vmin and vmax are calculated, the formula for turning design px into vw, the svh, dvh and lvh units that fix the mobile 100vh problem, why 100vw causes horizontal scrolling on desktop, container query units, and how viewport units behave when users zoom.

1. Viewport Unit Basics

1.1 The Four Core Units

  • vw (viewport width): 1% of the viewport's width
  • vh (viewport height): 1% of the viewport's height
  • vmin: 1% of the viewport's smaller dimension (= the smaller of 1vw and 1vh)
  • vmax: 1% of the viewport's larger dimension (= the larger of 1vw and 1vh)
/* Basic viewport unit examples */
.viewport-example {
  width: 50vw;        /* 50% of the viewport width */
  height: 30vh;       /* 30% of the viewport height */
  margin: 1vmax;      /* 1% of the larger dimension */
  border-radius: 2vmin; /* 2% of the smaller dimension */
}

1.2 How the Values Compute

/* Screen size: 1920x1080 */
.example-1920x1080 {
  width: 50vw;        /* 960px (1920 × 0.5) */
  height: 30vh;       /* 324px (1080 × 0.3) */
  font-size: 2vmin;   /* 21.6px (1080 × 0.02) - height is smaller */
  margin: 1vmax;      /* 19.2px (1920 × 0.01) - width is larger */
}

/* Screen size: 375x667 (iPhone SE 2nd/3rd gen screen; the actual browser viewport is shorter) */
.example-375x667 {
  width: 50vw;        /* 187.5px (375 × 0.5) */
  height: 30vh;       /* 200.1px (667 × 0.3) */
  font-size: 2vmin;   /* 7.5px (375 × 0.02) - width is smaller */
  margin: 1vmax;      /* 6.67px (667 × 0.01) - height is larger */
}

1.3 Converting Design px to vw

Convert px values from a design mockup to vw with this formula:

vw = px ÷ design width × 100
px = vw × screen width ÷ 100
  • 24px on a 375px mockup → 24 ÷ 375 × 100 = 6.4vw
  • 16px on a 360px mockup → 16 ÷ 360 × 100 ≈ 4.4444vw
  • 40px on a 1920px mockup → 40 ÷ 1920 × 100 ≈ 2.0833vw

On a 430px screen, that 6.4vw becomes 6.4 × 430 ÷ 100 = 27.52px, keeping the same proportion as the mockup. Common design widths are 360, 375 and 390px for mobile, 768 and 1024px for tablet, and 1440 and 1920px for desktop. Instead of calculating each value by hand, enter your design width into the PX to VW Converter.

2. vw and clamp() in Practice

2.1 Responsive Containers and Grids

/* 90% of the screen, capped at 1200px */
.container {
  width: min(90%, 1200px);
  margin-inline: auto;
}

/* A grid that never overflows on narrow screens */
.flexible-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: clamp(1rem, 0.8043rem + 0.8696vw, 1.5rem); /* 16px → 24px */
}

The container uses min(90%, 1200px) instead of width: 90vw because of the scrollbar problem explained in section 3. In the grid, min(300px, 100%) stops columns from overflowing their parent on screens narrower than 300px.

2.2 Fluid Typography

Every value below reaches its minimum at a 360px screen width and its maximum at 1280px. The preferred value includes a rem term so that browser zoom and the user's font-size setting still apply (see section 8).

/* Body: 16px → 18px */
.text-body {
  font-size: clamp(1rem, 0.9511rem + 0.2174vw, 1.125rem);
  line-height: 1.6;
}

/* Subheading: 18px → 24px */
.text-subheading {
  font-size: clamp(1.125rem, 0.9783rem + 0.6522vw, 1.5rem);
  line-height: 1.3;
}

/* Heading: 24px → 48px */
.text-heading {
  font-size: clamp(1.5rem, 0.913rem + 2.6087vw, 3rem);
  line-height: 1.2;
}

Here is how the heading values are derived: slope = (48 − 24) ÷ (1280 − 360) ≈ 0.026087, so the vw coefficient is 2.6087vw, and the intercept is 24 − 0.026087 × 360 ≈ 14.6087px = 0.913rem. At 360px that gives 14.6087 + 9.3913 = 24px; at 1280px, 14.6087 + 33.3913 = 48px.

2.3 A Sizing Scale with Custom Properties

:root {
  /* Spacing (360px → 1280px) */
  --space-sm: clamp(0.5rem, 0.4022rem + 0.4348vw, 0.75rem); /* 8px → 12px */
  --space-md: clamp(1rem, 0.8043rem + 0.8696vw, 1.5rem);    /* 16px → 24px */
  --space-lg: clamp(1.5rem, 0.913rem + 2.6087vw, 3rem);     /* 24px → 48px */

  /* Font sizes */
  --font-body: clamp(1rem, 0.9511rem + 0.2174vw, 1.125rem);
  --font-heading: clamp(1.5rem, 0.913rem + 2.6087vw, 3rem);
}

.section {
  padding-block: var(--space-lg);
  font-size: var(--font-body);
}

.section h2 {
  margin-bottom: var(--space-md);
  font-size: var(--font-heading);
}

3. 100vw and the Scrollbar

100vw includes the width of the vertical scrollbar. That's harmless where scrollbars overlay the content, as on macOS and mobile, but on desktops where the scrollbar takes up space, such as Windows, the element overflows by the scrollbar width (roughly 15–17px) and a horizontal scrollbar appears. For example, in a 1920px window with a 17px scrollbar, the content area is 1903px wide while a 100vw element is 1920px.

/* Problem: horizontal scrolling on Windows desktops */
.fullscreen-section {
  width: 100vw;
}

/* Fix 1: leave block elements at their default width, or use 100% */
.fullscreen-section {
  width: 100%;
}

/* Fix 2: size fixed elements with left/right */
.fixed-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
}

Setting html { scrollbar-gutter: stable; } always reserves space for the scrollbar, so the layout doesn't shift sideways between pages that scroll and pages that don't. The latest spec is moving toward subtracting the scrollbar width from vw when the gutter is reserved like this, but browsers are adopting it at different times, so using 100% is still safer than relying on 100vw.

4. The Mobile 100vh Problem: svh, lvh, dvh

4.1 Why 100vh Gets Cut Off

On current mobile browsers, vh is fixed to the height of the viewport with the address bar collapsed (the large viewport). The vh value does not change when the address bar moves. So when the page first loads with the address bar visible, the bottom of a 100vh element is pushed off screen. For example, on a device whose visible area is 660px with the address bar shown and 740px with it collapsed, 100vh is 740px, so the bottom 80px is hidden. This is the classic reason a button at the bottom of the first screen can't be seen.

4.2 svh vs lvh vs dvh

Unit Based on When the address bar moves Best for
vh Large viewport (equals lvh on mobile) Fixed Desktop-only layouts
svh Address bar expanded (smallest screen) Fixed Hero and first-screen sections
lvh Address bar collapsed (largest screen) Fixed Background images, decorative elements
dvh Currently visible area Updates live Modals, app shells and other UI that must fit exactly

Width and vmin/vmax have the same variants (svw, lvw, dvw, svmin, dvmax, and so on). There are also vi and vb, which follow the writing direction: in horizontal writing, vi equals vw and vb equals vh. On desktop browsers, where the address bar never collapses, vh, svh, lvh and dvh all have the same value.

4.3 Code for Each Case

/* First screen (hero): never cut off, even with the address bar visible */
.hero {
  min-height: 100vh;  /* for old browsers that don't know svh */
  min-height: 100svh;
}

/* Full-screen UI that must fit the visible area exactly (modals, app shells) */
.app-shell {
  height: 100dvh;
}

/* Backgrounds that shouldn't resize when the address bar moves */
.hero-background {
  height: 100lvh;
}

Use min-height rather than height for sections, so they grow with longer content instead of clipping it. Because dvh changes every time the address bar collapses or expands, it forces re-layout and can push the content below it around, so svh is the more stable default for ordinary sections. The old workaround of computing a --vh variable in JavaScript is no longer needed.

4.4 The On-Screen Keyboard and interactive-widget

By default, iOS Safari and Android Chrome (108 and later) don't shrink the layout viewport when the on-screen keyboard opens, so vh and dvh keep their values. If you want a dvh-based layout to shrink along with the keyboard, add interactive-widget=resizes-content to the viewport meta tag.

<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">

This option is supported in Chromium-based browsers but not in iOS Safari (as of October 2026). Always test UI that must sit directly above the keyboard, such as a chat input, on real devices.

5. Working with vmin and vmax

5.1 vmin: Sizes Independent of Orientation

vmin is based on the smaller of the width and height, so its value stays the same when the device rotates. On a 375x667 screen, 50vmin is 187.5px in both portrait and landscape (667x375). That makes it a good fit for square elements that must always fit on screen.

/* A square that always fits on screen */
.square-element {
  width: 50vmin;      /* 375x667: 187.5px / 1920x1080: 540px */
  height: 50vmin;
  border-radius: 2vmin;
}

/* Responsive avatar */
.responsive-avatar {
  width: 15vmin;
  height: 15vmin;
  border-radius: 50%;
  border: 0.5vmin solid #fff;
}

5.2 vmax: Elements That Cover the Screen

vmax is based on the larger of the width and height. A screen's diagonal is never more than √2 (about 1.414) times its longer side, so a 150vmax circle centered on screen covers the whole screen in any orientation. That's why it's common in circular reveal transitions.

/* A circle that covers the whole screen in any orientation */
.reveal-circle {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 150vmax;
  height: 150vmax;
  border-radius: 50%;
  transform: translate(-50%, -50%) scale(0);
  transition: transform 0.6s ease;
}

.reveal-circle.is-open {
  transform: translate(-50%, -50%) scale(1);
}

/* A logo that grows on larger screens, up to a cap */
.responsive-logo {
  width: min(25vmax, 300px); /* 375x667: 166.75px / 1920x1080: 300px */
  height: auto;
}

6. Layout Examples

6.1 A Full-Height Layout with Margins

.complex-layout {
  box-sizing: border-box;
  margin: 1rem;                        /* no width needed: block elements fill the space */
  min-height: calc(100svh - 2rem);     /* top + bottom margin, 1rem × 2 */
  padding: clamp(1rem, 0.6087rem + 1.7391vw, 2rem); /* 16px → 32px */
}

Writing width: calc(100vw - 2rem) invites both the scrollbar problem and padding miscalculations. A block element with only a margin fills the remaining width automatically.

6.2 Grid and Flexbox

/* A grid whose column count changes with screen size */
.responsive-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: clamp(1rem, 0.8043rem + 0.8696vw, 1.5rem);
}

/* Three-column flexbox */
.responsive-flex {
  --gap: clamp(1rem, 0.8043rem + 0.8696vw, 1.5rem);
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
}

.responsive-flex > * {
  flex: 1 1 calc((100% - 2 * var(--gap)) / 3);
  min-width: min(250px, 100%);
}

6.3 Sticky Footer

A sticky footer stays at the bottom of the screen even when the page content is short. Unlike a footer pinned with position: fixed, it never covers the content.

body {
  min-height: 100svh;
  margin: 0;
  display: grid;
  grid-template-rows: auto 1fr auto; /* header, main, footer */
}

6.4 Navigation Bar

.site-nav {
  position: sticky;   /* unlike fixed, doesn't cover the content */
  top: 0;
  z-index: 1000;
  box-sizing: border-box;
  min-height: 56px;   /* a fixed value instead of vh */
  padding: 0.75rem clamp(1rem, 0.6087rem + 1.7391vw, 2rem);
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #333;
}

.nav-logo {
  font-size: clamp(1.25rem, 1.1522rem + 0.4348vw, 1.5rem); /* 20px → 24px */
  font-weight: bold;
  color: white;
}

.nav-menu {
  display: flex;
  gap: clamp(1rem, 0.8043rem + 0.8696vw, 1.5rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav-link {
  font-size: clamp(1rem, 0.9511rem + 0.2174vw, 1.125rem);
  color: white;
  text-decoration: none;
}

.mobile-menu-toggle {
  display: none;
  font-size: 1.5rem;
  color: white;
  background: none;
  border: none;
}

@media (max-width: 768px) {
  .nav-menu {
    display: none;
  }

  .mobile-menu-toggle {
    display: block;
  }
}

A navigation bar set to height: 10vh is 39px on a phone in landscape (390px tall) and 144px on a 1440px-tall monitor, which looks wrong on both. Values that shouldn't depend on screen size, like bar heights, belong in px or rem.

7. Container Query Units (cqi)

Components like cards that appear in several places, such as a sidebar and the main column, are better sized to their parent than to the screen. Give the parent container-type: inline-size and its children can use cqi (1% of the container's inline size, i.e. its width in horizontal writing). Container query units have been supported in all major browsers since early 2023.

.card-list {
  container-type: inline-size;
}

.card {
  box-sizing: border-box;
  max-width: 400px;
  padding: clamp(1rem, 0.5rem + 4cqi, 2rem);          /* container 200px → 16px, 600px → 32px */
  border-radius: 12px;
  background: white;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}

.card-title {
  font-size: clamp(1.125rem, 0.875rem + 2.5cqi, 1.5rem); /* container 160px → 18px, 400px → 24px */
  margin-bottom: 0.5em;
}

.card-content {
  font-size: clamp(0.875rem, 0.8125rem + 1cqi, 1rem);    /* container 100px → 14px, 300px → 16px */
  line-height: 1.6;
}

Put the same card in a 300px sidebar and its title is 14 + 7.5 = 21.5px; put it in an 800px main column and it hits the 24px maximum. vw, which only knows the screen width, can't express that difference. If there is no eligible container, cqi falls back to the small viewport, like svw.

8. Zoom and Accessibility

When a desktop user zooms the browser (Ctrl/Cmd +), each CSS px gets larger and the viewport width measured in CSS px shrinks. As a result, text sized with vw alone stays almost the same size on screen when zoomed.

  • clamp(1rem, 2.5vw, 2rem): 32px in a 1280px window. At 200% zoom the viewport becomes 640px, so 2.5vw = 16px, shown at 2×, which is 32px on screen: no change
  • clamp(1rem, 0.75rem + 1.25vw, 2rem): 12 + 16 = 28px in a 1280px window. At 200% zoom it's 12 + 8 = 20px, shown at 2×, which is 40px on screen: about 1.43× larger

The larger the rem term's share of the preferred value, the closer the text follows the zoom level, and it also respects the user's default font-size setting. Pinch-zoom on mobile magnifies the whole page, so it isn't affected by this issue. For more on balancing clamp() and vw, see CSS clamp() vs vw. For values that should stay fixed, convert them to rem with the PX to REM Converter.

9. Performance and Browser Support

vw and vh are converted to px at computed-value time, so using them heavily has practically no performance cost. When the window resizes, layout is recalculated no matter which units you use. The one thing worth watching is dvh: on mobile it changes every time the address bar moves and triggers re-layout, so prefer svh for large section heights.

Feature Supported in all major browsers since
vw, vh, vmin, vmax Mid-2010s
clamp(), min(), max() 2020
svh, lvh, dvh and related units Late 2022
Container query units (cqi, etc.) Early 2023

As of 2026, the only fallback still worth writing is a vh line in front of a newer unit, as in min-height: 100vh; min-height: 100svh;. Feature detection like @supports (width: 1vw) is unnecessary.

10. Common Mistakes

Mistake Result Instead
width: 100vw Horizontal scrolling on Windows desktops width: 100% or left: 0; right: 0
height: 100vh on a hero Bottom hidden behind the mobile address bar min-height: 100svh
font-size: 2.5vw on its own Too small on mobile and doesn't grow with zoom clamp(1rem, 0.75rem + 1.25vw, 2rem)
vh for nav or footer height Too short in landscape, too tall on large monitors min-height in px or rem
minmax(20vw, 1fr) Column count stays fixed regardless of screen size minmax(min(240px, 100%), 1fr)
Padding in vw only Excessive whitespace on large screens clamp(1rem, 0.6087rem + 1.7391vw, 2rem)

11. Summary

In short, the 2026 defaults are: size things horizontally with clamp() that mixes vw with a rem term, use 100svh instead of 100vh for first-screen heights, and use 100% instead of 100vw for full-width elements. When a component needs to scale with its own space, reach for cqi. You can convert mockup px values with the PX to VW Converter and calculate fluid font sizes with the clamp() generator. For cases where vw does more harm than good, see When NOT to Use vw Units in CSS.

Try it out right awayPX to VW ConverterOpen →

Keep reading