Why 100vw Isn’t Always the Full Viewport

CSS viewport units have been around since 2012, and width: 100vw quickly became the go-to technique for full-width elements. But on desktop browsers with classic scrollbars, the math doesn’t always add up. When a vertical classic scrollbar is present, 100vw — and every other viewport unit — measures more than the actual visible width of the viewport. This discrepancy is the classic scrollbar problem, and it affects the new viewport units shipped in 2022 just as much as the original set.

The 2022 units were designed to solve a mobile-specific issue: sizing elements correctly as the browser’s retractable UI changes the viewport height during scroll. They do that job well. But they don’t address the desktop scenario where classic scrollbars consume part of the viewport’s width or height.

When the page has a vertical classic scrollbar, the length 100dvw is larger than the viewport width. All viewport units have this problem.

To understand why this happens and what can be done about it, it helps to break down the core concepts: the viewport types, zoom behavior, the initial containing block, and scrollbar variants.

Visual Viewport vs. Layout Viewport

The viewport is the browser’s rendering area. Its dimensions are measured in CSS pixels, not device pixels. On an iPhone SE in portrait, for example, the viewport is 375 by 548 CSS pixels when the browser UI is fully expanded. After the user scrolls and the UI retracts, the height grows to 626 CSS pixels. These are referred to as the small viewport size and large viewport size respectively.

The small and large viewport sizes on an iPhone in portrait and landscape modes
The small and large viewport sizes on an iPhone in portrait and landscape modes. (Large preview)

Desktop browsers can also resize the viewport — through window resizing, sidebar toggling, or page zoom — but they don’t have the small/large distinction that mobile browsers do.

In practice, there are two distinct viewports in a browser:

  • Visual viewport: the portion of the page currently visible on screen.
  • Layout viewport: the containing block for fixed-position elements.

These two viewports start out identical in size and position. They diverge in two situations:

  1. When the user pinch-zooms or double-taps to zoom in, the visual viewport shrinks (in CSS pixels) because it shows a smaller portion of the page, while the layout viewport size stays the same.
  2. When the virtual keyboard appears on mobile, the visual viewport height shrinks to fit above the keyboard, but the layout viewport height is unchanged.

One consequence of the two-viewport system: when a user zooms and pans, fixed-position elements don’t stick to the screen, which is almost always the desired behavior. There are exceptions — like floating action buttons that should sit above the virtual keyboard — and the CSS Working Group is discussing solutions for those cases.

CSS viewport units are based on the layout viewport, not the visual viewport. That distinction matters for all of the sizing math below.

Page Zoom vs. Pinch Zoom

The CSSOM View module defines two types of zoom:

“There are two kinds of zoom: page zoom, which affects the size of the initial viewport, and the visual viewport scale factor, which acts like a magnifying glass and does not affect the initial viewport or actual viewport.”

Page zoom — found in desktop browser menus — shrinks the layout viewport, causing the page to reflow. Responsive breakpoints react to it. Pinch zoom, available on all platforms, only scales the visual viewport; the layout viewport size doesn’t change and the page doesn’t reflow.

Page ZoomVisual Viewport Scale Factor
Available onDesktop platformsAll platforms
Activated byKeyboard command, menu optionPinch-to-zoom or double-tap gesture
ResizesBoth layout and visual viewportOnly visual viewport
Does it cause reflow?YesNo

The Initial Containing Block’s Role

The layout viewport is the containing block for fixed-position elements. Some developers prefer calling it the “position fixed viewport” because that’s what it effectively does.

/* this element completely covers the layout viewport */
.elem {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

For that common four-edged zero-offset pattern, inset: 0 is a concise replacement for setting top, bottom, left, and right individually. All major browsers have supported the inset property since April 2021.

The initial containing block (ICB) is a different rectangle. It sits at the top of the page with a static size equal to the small viewport size. When the page first loads, the layout viewport and the ICB are the same rectangle. They diverge only when the user scrolls: the ICB scrolls out of view while the layout viewport stays fixed and, on mobile, expands to the large viewport size.

Absolutely positioned elements — which by default are sized and positioned relative to the ICB — scroll out of view along with it.

/* this element completely covers the ICB by default */
.elem {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

The ICB is also the containing block for the <html> element. Because the ICB and layout viewport initially match, setting height: 100% on both <html> and <body> makes the body element exactly as tall as the initial viewport. That’s a common technique for keeping footers at the bottom of the initial viewport on sites like Google Search.

/* By default: ICB height = initial viewport height */

/* <html> height = ICB height */
html {
  height: 100%;
}

/* <body> height = <html> height */
body {
  margin: 0;
  height: 100%;
}

/* Result: <body> height = initial viewport height */
Layout ViewportInitial Containing Block
Containing block forposition: fixed elementsposition: absolute elements
Is it visible?Always in view (at least partially2).Scrolls out of view (positioned at the top of the page).
SizeSmall or large viewport size (depending on the browser UI)Small viewport size

The Full Family of Viewport Units

The original six viewport units shipped a decade ago. The newer variants — with s, l, and d prefixes — arrived in major browsers between Safari 15.4 (May 2022) and Samsung Internet 21 (May 2023). Some mobile browsers may still have inconsistent implementations of the new units.

Layout ViewportOriginal Units (2013)New Unit Equivalent (2022)
Widthvwsvw, lvw, dvw
Heightvhsvh, lvh, dvh
Inline Sizevisvi, lvi, dvi
Block Sizevbsvb, lvb, dvb
Smaller Sizevminsvmin, lvmin, dvmin
Larger Sizevmaxsvmax, lvmax, dvmax

Three points require clarification:

  • The inline and block sizes depend on writing direction. In left-to-right writing systems, vi equals vw and vb equals vh.
  • For vmin and vmax, "smaller" and "larger" refer to whichever dimension is smaller or larger. On a portrait phone, vmin corresponds to vw and vmax to vh.
  • Each unit is one-hundredth of the corresponding viewport size, so 100vw equals the full viewport width.

Each of the six original units now has three prefixed variants, bringing the total to 24 viewport units.

  • The s-prefixed units track the small viewport size. So 100svh is the height of the initial layout viewport when the browser UI is expanded.
  • The l-prefixed units track the large viewport size. 100lvh is the height after the UI retracts. The difference between values equals the collapsed UI height: 100lvh - 100svh.
  • The unprefixed units match the l-prefixed units in all browsers. For example, 100vh equals 100lvh.
  • The d-prefixed units track the current layout viewport size, whether small or large. 100dvh changes dynamically as the browser UI expands and retracts.

Why the New Viewport Units Exist

In February 2017, Chrome changed how the vh unit works on Android. Previously, vh had resized whenever the browser UI retracted or expanded during scrolling. After the change, vh became a static length based on the “largest possible viewport.” This matched behavior Apple had already shipped in iOS Safari as a compromise between two imperfect options. As a WebKit engineer described it, “using the larger view size was the best compromise,” since most sites using viewport units looked correct most of the time.

The side effect of this decision is that an element sized with height: 100vh becomes taller than the initially visible viewport, which represents the “small viewport size.” The same problem that affected iOS Safari thus spread to Chrome, prompting the CSS Working Group to introduce a set of new viewport units.

The small viewport unit svh solves the case of making a hero section fill the visible screen on page load. The same effect can be achieved with height: 100% on the element and all its ancestors, including <body> and <html>, but svh is more flexible.

Good use cases for the dynamic unit dvh are harder to find. Sizing elements with dvh risks constant layout shifts as the user scrolls. Two scenarios often suggested for it do not hold up on inspection:

  • Fixed-positioned elements: For modal dialogs and sidebars, height: 100% behaves the same as height: 100dvh, since the containing block for fixed elements is the layout viewport, which already has a height of 100dvh. The dvh unit is not needed to make such elements full-height.
  • Vertical scroll snapping: Setting snap pages to height: 100dvh produces a glitchy experience in current mobile browsers, though this could potentially be fixed by browser vendors.

Desktop browsers have no concept of small and large viewport sizes. All viewport units — old and new — represent the current size of the layout viewport, making width units equivalent (vw = svw = lvw = dvw) and height units equivalent (vh = svh = lvh = dvh). Swapping 100vh for 100svh would change nothing on desktop. The same equivalence can occur on mobile when a page is embedded in an <iframe> or when an installed web app runs in standalone mode.

Even during regular browsing on mobile, the small and large viewport sizes can match. Two cases exist:

  1. In iOS Safari, choosing “Hide Toolbar” from the page settings menu keeps the browser UI retracted during scrolling and navigation.
  2. In Firefox on Android, disabling “Scroll to hide toolbar” under Settings → Customize stops the UI from retracting entirely.

Classic vs. Overlay Scrollbars

Browsers display one of two scrollbar types: classic or overlay. Mobile platforms use overlay scrollbars exclusively. On desktop, the type depends on the operating system settings, with options typically labeled “Always show scrollbars.” Windows defaults to classic; macOS defaults to overlay but switches when a mouse is connected.

The distinguishing behavior is spatial: classic scrollbars occupy a “scrollbar gutter” that consumes space and shrinks the layout viewport, while overlay scrollbars sit on top of the page content. In a desktop browser with classic scrollbars, a vertical scrollbar appearing can shrink the viewport by the gutter width — normally 15 to 17 CSS pixels — causing a reflow. Browsers show classic scrollbars only when content overflows unless the CSS overflow property forces them.

The scrollbar-gutter: stable declaration on the <html> element prevents reflow by instructing the browser to reserve gutter space permanently. It has no effect where overlay scrollbars are used, and Safari does not support it at the time of writing.

Classic scrollbars have a discoverability advantage: they make scrollable overflow visible. Overlay scrollbars remain hidden until the user attempts to scroll. Users may not realize that an element contains more content below the fold. Chrome for Android partially mitigates this by displaying the overlay scrollbar until the element has been scrolled at least once.

Even if future Windows versions default to overlay scrollbars, some users will still enable classic scrollbars where possible. Testing with classic scrollbars enabled will remain necessary to keep sites usable.

Two Pitfalls With Classic Scrollbars

In a desktop browser with classic scrollbars, the most noticeable issues are unexpected extra scrollbars caused by minor overflow and empty scrollbar tracks. Neither is critical, but both make a page look unfinished and can annoy visitors.

Pitfall 1: overflow: scroll Instead of auto

Whether a scroll container actually overflows depends on content length, viewport width, and other factors. When overflow is absent, hiding the scrollbar is preferable to leaving an empty track. Setting overflow: auto provides this behavior.

Developers working on macOS, with its overlay scrollbars, may habitually write overflow: scroll. Overlay scrollbars behave identically for both auto and scroll values. The difference appears only with classic scrollbars, where overflow: scroll unconditionally shows an empty track. Using auto trades one trade-off for another — content may reflow — but that side effect is addressable with scrollbar-gutter: stable.

Pitfall 2: Media Query Widths Overstate Available Space

CSS media queries act as if scrollbars never exist. They report the viewport width including the space a classic scrollbar would occupy. For example, with a layout viewport of 983 pixels and a 17-pixel vertical classic scrollbar, the media query (min-width: 1000px) evaluates to true because 1000 pixels would be available if the scrollbar disappeared.

This behavior is intentional, preventing infinite loops that would occur if showing a scrollbar broke a breakpoint. Puzzlingly, Safari behaves differently; it accounts for scrollbars in media queries and can trigger breakpoint changes when a scrollbar appears, though it includes safeguards against loops. Apple's behavior is treated as a bug in WebKit.

The practical consequence for developers: never assume that the full width named by a media query is visible, and avoid setting width: 1000px inside a @media (min-width: 1000px) rule.

Pitfall 3: 100vw Overflows With a Classic Scrollbar

The length 100vw equals the layout viewport width except on pages showing a vertical classic scrollbar, where it is larger. Setting an element to width: 100vw in that case causes slight horizontal overflow. This anomaly is known; the CSS Values and Units spec notes, “Level 3 assumes scrollbars never existed because it was hard to implement, and only Firefox bothered to do so.”

Firefox once behaved differently: on pages setting overflow: scroll on the root element, 100vw matched the actual viewport width. That behavior was removed from Firefox in 2017 and later stripped from the specification. Recently, the CSS Working Group reversed course and resolved to re-add the behavior, accounting for the root's default scrollbar and scrollbar-gutter properties. The change has not yet entered the specification, and browser support will take time.

Why the New Units Didn’t Fix Scrollbars

Given the title of this article, you might expect that the new viewport units were designed to address the classic scrollbar issue. They weren't. In practice, the new svw, dvw, and lvw units all equal the original vw unit in browsers—meaning 100svw, 100dvw, and 100lvw are identical to 100vw. This might initially look like a missed opportunity to finally let developers size elements to the actual viewport width.

There are two primary reasons the new units don't solve this classic problem:

  1. The new units were created to address the mobile browser issue where 100vh is taller than the visible viewport when the browser's UI is expanded. That situation is fundamentally different from a desktop viewport shrinking due to a classic scrollbar. Using the same s-prefixed unit for both would lead to unwanted side effects—fixing a mobile layout issue would break a desktop layout, and vice versa.
  2. The CSS Working Group maintains that viewport units must be "resolvable at a computed-value time" and should "not depend on layout." Implementing units that depend on layout is "relatively hard" for browsers.

Instead, the CSS Working Group has decided to mitigate the issue by making 100vw smaller in browsers with classic scrollbars when the scrollbar-gutter property is set to stable on the <html> element. The logic is straightforward: when a stable scrollbar gutter is reserved, the viewport width doesn't change when the scrollbar appears, so 100vw can safely match the viewport width at all times. Once browsers implement this, developers can use scrollbar-gutter: stable to prevent horizontal overflow from a full-width element.

/* THIS BEHAVIOR HAS NOT YET SHIPPED IN BROWSERS */

/* On pages with a stable scrollbar gutter */
html {
  scrollbar-gutter: stable;
}

/* 100vw can be safely used */
.full-width {
  width: 100vw;
}

Working Around the Viewport Width Problem

For years, developers have used JavaScript-updated CSS custom properties to get the real viewport size. The custom property --vw below is a dynamic stand-in for the vw unit, correctly recalculated whenever a classic scrollbar appears or disappears. If JavaScript fails, the variable falls back to 1vw.

.full-width {
  width: calc(var(--vw, 1vw) * 100);
}

The JavaScript uses document.documentElement.clientWidth, which returns the ICB width (the layout viewport's width). Since the resize event doesn't fire when a scrollbar alters the viewport's width, a resize observer on the <html> element is needed instead.

new ResizeObserver(() => {
  let vw = document.documentElement.clientWidth / 100;
  document.documentElement.style.setProperty('--vw', `${vw}px`);
}).observe(document.documentElement);

Container queries provide a JavaScript-free alternative. By making the <body> element an inline-size query container, you can use 100cqw (the width of <body>) instead of 100vw. Unlike 100vw, 100cqw automatically shrinks when a vertical classic scrollbar appears.

body {
  margin: 0;
  container-type: inline-size;
}

.full-width {
  width: 100vw; /* fallback for older browsers */
  width: 100cqw;
}

Container queries have been available in all major desktop browsers since February 2023. If the page has nested query containers, you can store the <body> width in a registered custom property to access it inside those containers. Note that Firefox still doesn't support registered custom properties at the time of writing.

Further Exploration

For a deeper dive, the viewport investigation project—a joint effort by browser vendors to "research and improve the state of interoperability of existing viewport measurement features"—is a great resource. The new viewport units were a focus area of Interop 2022.

Smashing Editorial