TLDR: On iOS Safari, 100vh is taller than the visible screen. Use 100dvh instead. One unit, one fix.

the setup

I was rebuilding my site from scratch — Next.js 16, Tailwind 4, the whole thing.

One of the first things I built was a kinetic scroll-driven hero. Full-bleed, pinned to the viewport, a little animated.

On desktop it looked incredible.

I was genuinely excited about this one.

the wall

Then I pulled it up on my iPhone.

The hero was taller than the visible screen. Content was sliding behind Safari's bottom toolbar — the tab bar, the nav pill — all of it eating into my layout.

My "pinned" section wasn't pinned. It was overflowing.

what I tried first (wrong)

My first instinct was a padding hack. Add some bottom padding to compensate for the toolbar height.

That doesn't work, because the toolbar collapses and expands as you scroll. Any fixed number breaks it on every other device.

I also looked at min-height: -webkit-fill-available — the old iOS workaround. It works, kind of. But it's a vendor-prefix hack I didn't want load-bearing a whole site redesign.

There had to be a cleaner answer.

what's actually happening

Here's the thing about vh on iOS Safari: it doesn't do what you think it does.

100vh is calculated at page load with the browser chrome hidden — meaning it uses the largest possible viewport, not what the user actually sees when they land on your page.

So when Safari's address bar is visible (which it is, on load), your 100vh element is taller than the screen. Content gets buried. Browser chrome sits on top.

This has been the behavior for years. It bites everyone eventually.

the fix that worked

CSS has had the right answer since 2022: dvhdynamic viewport height.

dvh updates in real time as the browser chrome shows and hides. 100dvh always matches the actual visible area.

The fix in my code was literally this:

/* before */
min-height: 100vh;

/* after */
min-height: 100dvh;

Safari 15.4+, Chrome 108+, Firefox 101+. Well-supported enough for production today.

One unit. Done.

why it matters to me

I got burned here because I tested on desktop and assumed mobile would follow.

It didn't — and I wouldn't have caught it without pulling the site up on a real iPhone and then running a full mobile pass through Arc CDP (my browser automation tool for driving Arc with a real mobile viewport) to catch every page.

The honest lesson: mobile viewport testing is not optional. vh is the legacy trap. dvh is the right answer. Swap them everywhere you're pinning something to the viewport.

Your users are mostly on phones. Ship for the device they're actually holding.

P.S. — svh (small, chrome fully visible) and lvh (large, chrome hidden) exist if you need that predictability. But for a hero or any pinned section, dvh is what you want.