Fixes8 min read·

100vh on mobile is broken: use dvh, svh and lvh

100vh hides your content behind Safari and Chrome toolbars on phones. Here's why, with real iPhone and Android numbers, and how dvh, svh and lvh fix it.

On phones, 100vh is the height of the viewport with the browser toolbars tucked away, but pages load with the toolbars showing. So a 100vh section is taller than what people can actually see, and its bottom (often your main button) hides behind the toolbar. Use 100svh for sections that must fit on first load, 100dvh for app-style layouts that should follow the toolbars, and keep 100vh as a fallback line.

What's actually going on

Mobile browsers have toolbars that change size. On first load they're fully expanded. Start scrolling and they shrink or slide away to give you more page. Scroll back up and they return.

Real numbers make this clearer:

Device and browser Screen (CSS px) Page visible on first load Extra page after scrolling
iPhone 16 Pro / iPhone 17 Pro, Safari (iOS 26) 402×874 718px (measured) about 36px
Galaxy S25, Chrome 360×780 about 660px (estimate) about 80px
Pixel 10, Chrome 412×924 about 786px (estimate) about 80px

Where those numbers come from:

  • Safari on iPhone (compact tab bar, the iOS 26 default): on a 402×874 iPhone 16 Pro, 718px of page is visible on first load. After scrolling, the bars shrink and the page gains roughly 36px. The iPhone 17 Pro has the same 402×874 screen.
  • Chrome on Android: a status bar at the top (40px on typical phones, 58px on the Pixel 10), a 56px Chrome toolbar and a 24px gesture bar at the bottom. After scrolling, the toolbar and the gesture chin hide and the page gains about 80px. So the Galaxy S25 estimate is 780 − 40 − 56 − 24, and the Pixel 10 is 924 − 58 − 56 − 24. The iPhone, Samsung Galaxy and Google Pixel pages list every phone's screen size if you want to run the numbers for others.

Now the problem. On phones, Safari and Chrome size vh for the toolbars-hidden state, the bigger one. On first load the toolbars are showing, so 100vh is taller than the visible area and the bottom slice of your "full screen" hero sits under the browser bar. web.dev's article on the new viewport units shows it happening side by side.

There's more on how much of an iPhone screen is really usable in How much of the screen iPhone users see in Safari.

The new units: svh, lvh and dvh

CSS now has three sets of viewport units, one for each state. The definitions, from the CSS spec via web.dev:

  • Small viewport (svh): the viewport sized as if the browser's toolbars are expanded.
  • Large viewport (lvh): the viewport sized as if the toolbars are retracted.
  • Dynamic viewport (dvh): whatever the viewport is right now. It equals the small viewport when the bars are out and the large viewport when they're tucked away.

In plain English:

Unit Means On first load Good for
100svh the smallest the viewport gets exactly fits what's visible heroes, splash screens, anything that must fit on arrival
100lvh the largest the viewport gets taller than what's visible backgrounds that must never show a gap
100dvh the current size fits, then grows as bars hide app shells, full-screen menus, modals
100vh on phones, behaves like lvh taller than what's visible fallback line only

Each comes with width and min/max versions too (svw, dvw, svmin, dvmax and so on), but the heights are the ones you'll use.

Support is not a worry any more: Safari since 15.4, Firefox since 101, Chrome and Edge since 108. On desktop there are no sliding toolbars, so all four are the same number there.

The fallback pattern

Write the old unit first and the new one second. Browsers that don't understand the new unit ignore that line and keep the first:

.hero {
  min-height: 100vh;   /* very old browsers */
  min-height: 100svh;  /* fits the screen on first load */
}

.app-shell {
  height: 100vh;
  height: 100dvh;      /* tracks the toolbars */
}

If you prefer being explicit, @supports does the same job:

.app-shell { height: 100vh; }

@supports (height: 100dvh) {
  .app-shell { height: 100dvh; }
}

Tailwind: h-screen and min-h-screen are 100vh. Since Tailwind v3.4 you can use h-dvh, h-svh, h-lvh, min-h-dvh, min-h-svh and min-h-lvh instead. For most AI-built landing pages, swapping min-h-screen for min-h-svh on the hero is the whole fix.

height or min-height?

This matters as much as the unit.

Use min-height for content sections: heroes, landing panels, "full screen" intros. min-height: 100svh means "at least one screen tall, but grow if the content needs it". On a 320px-wide Samsung on its largest display size, your headline wraps onto more lines and needs more room. With height it would overflow or get clipped. With min-height the section just gets taller.

Use height for app layouts where the page itself shouldn't scroll and an inner area does: chat apps, dashboards, maps, editors, full-screen menus and modals. Here height: 100dvh keeps the layout exactly the size of what's visible, even as the toolbars come and go.

Why not dvh everywhere? Because it changes while people scroll. A big 100dvh hero grows as the toolbars hide, which can nudge everything below it. Browsers also throttle these updates rather than recalculating on every frame, so the resize can look a bit jumpy. svh never changes, so it's the calmer choice for anything that isn't an app shell.

Fixed bottom bars and the home indicator

A sticky "Buy now" or cookie bar at the bottom of the screen has two enemies: the browser toolbar and the phone's home indicator or gesture bar.

For the bar itself you don't need viewport-height units at all: position: fixed with bottom: 0 does the positioning. What you do need is room for the home indicator. Opt in to edge-to-edge with viewport-fit=cover (more on that in The meta viewport tag), then pad with the safe-area inset:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
.bottom-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 12px 16px;
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
}

/* Leave room so the bar doesn't cover the end of the page */
main {
  padding-bottom: calc(72px + env(safe-area-inset-bottom));
}

env(safe-area-inset-bottom) is 0 on phones without a home indicator, so this is safe everywhere. Chrome on Android uses the same viewport-fit=cover opt-in: from Chrome 135 the page can extend under Android's gesture bar, and the inset keeps your buttons clear of it (Chrome's edge-to-edge guide).

The keyboard is a separate problem

The on-screen keyboard doesn't count as browser toolbar, so it doesn't change svh, lvh or dvh. Since Chrome 108, Chrome on Android behaves like Safari here: the keyboard shrinks only the visual viewport and your layout stays put (Chrome's write-up).

If you need an input pinned above the keyboard, as in a chat app, you have two options: add interactive-widget=resizes-content to the viewport tag (supported in Chrome on Android), or read window.visualViewport.height in JavaScript and position things yourself.

Old hacks you can retire

Before these units existed, people used workarounds. You'll still find them in templates and AI-generated code.

The -webkit-fill-available trick:

/* Old workaround */
body {
  min-height: 100vh;
  min-height: -webkit-fill-available;
}

It was a clever fix for Safari in its day, but it's non-standard and browsers don't treat it consistently. Now that svh and dvh work everywhere, there's no reason to add it to new code. If it's already in your project and working, replace it with the fallback pattern above when you next touch that CSS, and check the result on a real phone.

The JavaScript --vh trick:

// Old workaround
document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`);

paired with height: calc(var(--vh) * 100). It works, but it depends on JavaScript, has to re-run on every resize, and the layout can jump when it does. 100dvh does the same job in plain CSS.

Test it where the toolbars are

This bug is invisible on desktop and in most simple previews, because there are no toolbars. You need to see the browser bars.

  • On a real phone: load the page fresh (not scrolled) and check that your hero's button is visible without scrolling. How to open localhost on your phone shows how to do that before you deploy.
  • In doesitfit: turn on Browser bars in the app and each phone preview gets Safari's or Chrome's toolbars at their real heights, so you can see exactly what's hidden on first load across dozens of phones at once.
  • Without a phone: How to test your website on mobile without a phone covers the options and their limits.

Does your site fit?

Paste your address (or localhost:3000). doesitfit shows it on real screen sizes at once and tells you, bluntly, where it spills.

Free, no sign-up. Or ask Claude, ChatGPT or Codex to check it.

A prompt for your AI tool

Find every place my project sets a full-screen height: 100vh, Tailwind h-screen or min-h-screen, -webkit-fill-available, or a JavaScript --vh variable. For content sections like the hero, use min-height: 100vh followed by min-height: 100svh (Tailwind: min-h-svh). For app-style layouts where an inner area scrolls, use height: 100vh followed by height: 100dvh (Tailwind: h-dvh). Remove the old -webkit-fill-available and --vh workarounds once replaced. If there's a fixed bottom bar, add viewport-fit=cover to the viewport meta tag and padding-bottom: calc(12px + env(safe-area-inset-bottom)) to the bar. Show me each change.

What to remember

  1. 100vh on phones is the toolbars-hidden height, so it's too tall on first load.
  2. min-height: 100svh for sections that must fit on arrival.
  3. height: 100dvh for app shells, menus and modals.
  4. Always put the vh line first as a fallback.
  5. Pad fixed bottom bars with env(safe-area-inset-bottom) after opting in with viewport-fit=cover.
  6. Test with the toolbars visible, on a fresh load, on a real phone or with browser bars turned on. For more on the difference between the viewport and the screen, see What is a viewport?.

Questions people ask

Does dvh work on iPhone?

Yes. Safari has supported svh, lvh and dvh since Safari 15.4, Chrome and Edge since 108 and Firefox since 101, so every current browser understands them.

Why does 100vh work on desktop but not on phones?

Desktop browsers don't slide their toolbars in and out as you scroll, so there is only one viewport height and vh, svh, lvh and dvh are all equal. Phones have two heights, and vh uses the bigger one.

Should I replace every vh in my CSS with dvh?

No. Use svh for sections that must fit on first load, dvh for app-style layouts that should track the toolbars, and leave vh where the exact height doesn't matter. Keep a vh line before each new unit as a fallback.

What's the difference between h-screen and h-dvh in Tailwind?

h-screen sets height: 100vh, which is too tall on phones when the browser bars are showing. h-dvh sets height: 100dvh, and min-h-svh sets min-height: 100svh; both were added in Tailwind v3.4.

Keep reading