100vh Isn't What You Think
A full-height hero, cut off at the bottom on the one browser where "full height" doesn't mean what it says.
min-height: 100vh is one of the most common lines in
any hero section's CSS, and on desktop it does exactly what it
looks like it does - fill the screen. On mobile Safari, and to a
lesser extent mobile Chrome, it doesn't. The unit isn't buggy in
the sense of behaving randomly; it's doing exactly what the spec
says. The spec just doesn't mean what most people assume it
means, and that gap is what causes a hero's call-to-action button
to sit just below the fold, or a page to visibly jump the instant
someone starts scrolling.
On a phone, the browser chrome - the address bar, the tab strip, the bottom toolbar - takes up real space, and it isn't static. It collapses when you scroll down and reappears when you scroll back up or land on a fresh page. That means the actual visible area of the page keeps changing size by the height of that chrome, often 60-100px, without the page itself resizing.
100vh doesn't track that. It's defined against the
largest possible viewport - the one with the browser
chrome fully collapsed - and it stays pinned to that value
regardless of whether the chrome is actually showing right now.
On first load, with the address bar visible, an element set to
min-height: 100vh is taller than the space actually
available, so the bottom of it - often exactly the button or
headline meant to close out the hero - sits below what's
currently on screen.
100vh against.
That last point is what makes this easy to ship without noticing - the bug is specific to real mobile browsers with dynamic chrome, not to narrow viewport width, so a resized desktop browser window won't reproduce it.
Before touching anything, the fix has to match the actual
cause. Cutting the affected section's own padding, margin, and
flex rules down to the minimum and testing on a real device -
not an emulator - confirmed the section itself was fine; only
elements using vh units were affected.
The long-standing fix for this is a small script that reads
window.innerHeight, writes it to a CSS custom
property, and re-runs on every resize event, with every
100vh reference swapped for
calc(var(--vh, 1vh) * 100). It works, but it adds a
runtime dependency and a resize listener to fix something CSS
can now handle natively, and it still renders wrong for one
frame before the script runs.
svh as a fallback, not a
replacement
Modern browsers support svh (small viewport
height) - the viewport size with browser chrome
visible, which is the value that was actually needed all
along. Replacing 100vh outright would break
older browsers that don't recognize the unit and would simply
drop the declaration. Declaring 100vh first and
100svh immediately after solves that: browsers
that understand svh use it, since it's the later
declaration for the same property; browsers that don't just
ignore that line and keep the vh value.
A single fixed hero doesn't help much if the same
min-height: 100vh pattern is copy-pasted across
every other page's hero section too. The actual fix was an
audit - every occurrence of the raw property, across the
shared stylesheet and every page-specific override, got the
same two-line treatment, not just the page someone happened to
be looking at when this came up.
min-height: 100vh computed against the largest
possible viewport
min-height: 100svh declared right after the
vh value, same property
vh behavior
One extra line per rule:
min-height: 100vh; min-height: 100svh;
The fix itself is trivial. What took the actual time was finding
every place the pattern already existed - a shared stylesheet
rule, plus a handful of page-specific overrides that had been
copy-pasted with their own min-height: 100vh - and
confirming each one got the same treatment, rather than fixing
the one hero someone happened to notice and leaving the rest
inconsistent. Catching mobile-specific rendering issues like this
one is a standard part of the
Web & Digital work done on
every build.
vh is defined inconsistently across mobile browsers
in practice, usually against the largest viewport.
svh (small viewport height) is the viewport with
browser chrome fully visible - the safe, conservative value.
lvh (large viewport height) is the viewport with
chrome fully collapsed - functionally close to how
vh already behaves on mobile.
dvh (dynamic viewport height) tracks the chrome in
real time and resizes as it collapses or reappears, which fixes
the cutoff but reintroduces the layout-shift-on-scroll behavior
this fix is meant to avoid. svh is the one that
actually solves the "content cut off" problem without adding
movement.
No, for any browser that supports svh - which is
every current major mobile browser. The fallback declaration
handles the rest: browsers without svh support
simply keep using vh, the same way they already
were before this fix existed.
No. Desktop browsers don't have collapsing chrome that changes
the viewport size, so vh and svh
resolve to the same value there. The fallback declaration is
inert on desktop - it's purely a mobile fix, with no visible
change anywhere else.
Desktop responsive mode misses this one. A quick look on an actual device is usually enough to tell.