Guides

Why 100vh is taller than the screen on mobile — 3 causes and how to fix them

Illustration: a tall golden slab doesn't fit inside the phone window and pokes out at the bottom while Kodik measures it with a tape

You built the first screen with height: 100vh. A heading, an image, a "Get started" button at the bottom. On your computer — perfect. On a phone, the button isn't there. It only shows up after you scroll a little.

And the worst part: in DevTools phone mode everything looks fine. web.dev says it directly: in browsers without sliding toolbars, like Chrome on desktop, all the viewport heights are the same. The bug only lives on a real phone.

Here are three causes, from most common to least.

Cause 1. vh is measured against the "large" screen

A mobile browser has two screen heights. While the address bar is visible, the screen is smaller. When you scroll down and the bar hides, it's larger.

vh uses the larger one. Chrome on Android switched to this back in version 56, in 2017, to behave like Safari on iPhone. The Chrome team wrote it plainly: 100vh will be larger than the visible height while the URL bar is shown.

So at load the bar is visible, but the block is sized as if it weren't. The bottom of the block goes past the edge.

How to check. Open the site on a phone and scroll a little. If the button appears exactly when the address bar hides, this is it.

How to fix it. Since 2022, every major browser has new units (Chrome 108, Firefox 101, Safari 15.4):

  • svh — the small screen, browser bars visible;
  • lvh — the large screen, bars hidden. That's good old vh;
  • dvh — the dynamic height: it changes as the bars slide in and out.

For the first screen, use svh:

.hero {
  min-height: 100vh;
  min-height: 100svh;
}

The first line is a fallback for older browsers. The second overrides it wherever svh is understood. And min-height instead of height lets the block grow if the text doesn't fit.

Why not dvh? It changes along with the bars: while someone scrolls, the block grows and shrinks, and everything below it shifts. On top of that, web.dev warns that the dynamic height doesn't update every frame; it lags behind. Keep dvh for full-screen interfaces like a chat, where the height has to match exactly.

In Tailwind, the h-screen class is exactly 100vh. Replace it with min-h-svh or h-dvh: those classes exist since version 3.4.

Cause 2. The keyboard covers the bottom

A chat input is pinned to the bottom of the screen. You tap it, the keyboard slides up and covers the field.

web.dev explains: the on-screen keyboard isn't considered a browser bar and doesn't change the viewport units. And since Chrome 108, the keyboard on Android shrinks only the visible area, not the page layout. Safari on iPhone has long worked the same way.

How to check. Tap the field at the bottom. If it stays under the keyboard, this is the cause.

How to fix it. In Chrome on Android, you can ask for the old behavior with one key in the meta tag:

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

Now, with the keyboard open, the layout shrinks and the bottom bar sits above it. But when the Chrome team introduced this key, they warned it only works in Chrome. On iPhone even Chrome doesn't understand it — it runs on Safari's engine there. So for iPhone you read the visible area's height in JavaScript:

const vv = window.visualViewport;

function syncHeight() {
  document.documentElement.style.setProperty('--app-height', `${vv.height}px`);
}

vv.addEventListener('resize', syncHeight);
syncHeight();

And in CSS the chat container gets height: var(--app-height, 100dvh).

Cause 3. The bottom slides under the gesture bar

The button is technically on screen, but half of it sits under the home indicator on iPhone or under the Android navigation bar.

This happens when the meta tag has viewport-fit=cover. The page stretches to the very edges, and the browser stops moving it away from system elements. Since version 135, Chrome on Android can do this too.

How to check. Look for viewport-fit=cover in <meta name="viewport">. If it's not there, this cause isn't yours.

How to fix it. Add bottom padding from the safe area:

.bottom-bar {
  padding-bottom: max(16px, env(safe-area-inset-bottom));
}

What these insets are and where they come from is covered in detail in what is the safe area.

When you ask AI to build a mobile screen, add one line to the prompt: "don't use 100vh for height — use 100svh with a 100vh fallback, and respect the safe area." The first and third causes are handled up front. The basic mobile setup is in how to make your site mobile-friendly.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app

Do I still need the old --vh and innerHeight hack?

For a regular screen height, no. It was invented when svh and dvh didn't exist. Today a 100vh fallback line is enough. JavaScript is only still needed for the keyboard on iPhone.

Why doesn't position: fixed with height: 100% have this problem?

The Chrome team described it directly: a fixed element with 100% height always takes exactly the visible height and resizes along with the address bar. That's handy for menus and modals. But a first screen lives in the normal page flow — it needs svh.

svh or dvh — which should be the default?

svh. The block will never slide under the bars and won't jitter while scrolling. Use dvh only where the interface must fill exactly the visible height.

What if the styles don't apply at all?

Then it's not about vh. Check whether the rule reaches the element with the checklist in why your CSS isn't working.

KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →