Viewport units have always been controversial and some of that is because of how mobile browsers have made things more complicated by having their own opinions about how to implement them.
Case in point: should the scrollbar be taken into account for the vw
unit? What about a site’s navigation or page controls — should those count in the calculation? Then there are physical attributes of the devices themselves (hello, notch!) that can’t be overlooked.
First, a little context
The spec is pretty vague about how viewport units should be calculated. With mobile devices, we’re often concerned with the vertical height, so let’s look specifically at viewport height (vh
):
vh unit
Equal to 1% of the height of the initial containing block.
So yeah, no clear guidance there when it comes to handling device and browser-specific differentiations.
vh
was initially calculated by the current viewport of your browser. If you opened your browser and started to load a website, 1vh
was equal to 1% of your screen height, minus the browser interface.
But! If you start scrolling, it’s a different story. Once you get past a piece of the browser interface, like the address bar, the vh
value would update and the result was an awkward jump in the content.
Safari for iOS was one of the first mobile browsers to update their implementation by choosing to define a fixed value for the vh
based on the maximum height of the screen. By doing so, the user would not experience jumps on the page once the address bar went out of view. Chrome’s mobile browser followed suit around a year ago.
As of this writing, there is a ticket to address this in Firefox Android.
While using a fixed value is nice, it also means that you cannot have a full-height element if the address bar is in view. The bottom of your element will be cropped.

CSS Custom Properties: The trick to correct sizing
The idea struck me that CSS Custom Properties and a few lines of JavaScript
might be the perfect way to get the consistent and correct sizing I needed.
In JavaScript, you can always get the value of the current viewport by using the global variable window.innerHeight
. This value takes the browser’s interface into account and is updated when its visibility changes. The trick is to store the viewport value in a CSS variable and apply that to the element instead of the vh
unit.
Let’s say our CSS custom variable is --vh
for this example. That means we will want to apply it in our CSS like this:
.my-element {
height: 100vh; /* Fallback for browsers that do not support Custom Properties */
height: calc(var(--vh, 1vh) * 100);
}
OK, that sets us up. Now let’s get the inner height of the viewport in JavaScript:
// First we get the viewport height and we multiple it by 1% to get a value for a vh unit
let vh = window.innerHeight * 0.01;
// Then we set the value in the --vh custom property to the root of the document
document.documentElement.style.setProperty('--vh', `${vh}px`);
We told JavaScript to grab the height of the viewport and then drilled it down into 1/100th of that total so we have a value to assign as our viewport height unit value. Then we politely asked JS to create the CSS variable (--vh
) at the :root
.
As a result, we can now use --vh
as our height value like we would any other vh
unit, multiply it by 100 and we have the full height we want.
There is another fix for this that has come along more recently. Matt Smith documents it here. The trick is min-height: -webkit-fill-available;
on the body as a progressive enhancement over 100vh
, which should work on iOS devices.
Whoa, there! One more little detail.
While our work might look done at this point, those of you with an astute eye for detail may have caught that the JavaScript fires but never updates the size of our element when the viewport’s height changes. Go ahead and try resizing the demo above.
We can update the value of --vh
by listening to the window resize
event. This is handy in case the user rotates the device screen, like from landscape to portrait, or the navigation moves out of view on scroll.
// We listen to the resize event
window.addEventListener('resize', () => {
// We execute the same script as before
let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
});
⚠️ Updating the value of --vh
will trigger a repaint of the page and the user may experience a jump as a result. Because of this, I’m not advising that this trick should be used for every project or to replace all usage of the vh unit but only when you may need your users to have an exact viewport unit value.
Also, you may want to implement a debounce method for the resize event to avoid triggering to many events while the user is resizing their browser’s window. You can learn more about it with this article: Debouncing and Throttling Explained Through Examples
You can now resize the demo above and notice that the CSS variable is updated accordingly.
While I recently used this technique on a project and it really helped, you should always think twice when replacing the browser’s default behaviors. (For example, this comes up a lot with ::focus
.) Also, browsers tend to update very fast these days, so beware that today’s solution may not work tomorrow.
In the meantime, I hope this article helps! 👋
Here’s a proposal for vhc
and vwc
units that may be a savior in all this.
You should really use some kind of throtteling when listening to the resize event especially if it is triggering a repaint — for example like documented here: https://devdocs.io/dom_events/resize
Thanks for your feedback, I added a small note the debounce technique but I didn’t want to add extra code into the demos to keep them clear ✌️
Haven’t tried that yet but I struggled with that problem for months! Thank you very much will try this to fix my website soon.
Could you elaborate your use case? You need an in-flow full-height element at the top of the page?
I’ve read somewhere (I think, on nngroup.com) that it’s best to make such an element slightly smaller, so that the user knows that they can scroll down. (Apparently, some users will assume that there is no content below.)
That’s exactly the use case where I needed it. The first screen of the website was supposed to be full-height on mobile but we got the bottom cropped (which was the client’s logo).
You could also use this trick if you have a modal that should be 100vh and you don’t want your users to loose the bottom part because of the browser’s interface.
I was struggling with this exact issue a month ago and came up with similar solution :D
I wasn’t doing –vh in root but in that element that needed vh unit only and with jQuery as the project was in jQuery. But the concept is the same.
Similar fix width modal overlay, when body tag overflow hidden:
in js:
function getScrollbarWidth() {
return window.innerWidth – document.documentElement.clientWidth;
}
document.documentElement.style.setProperty(‘–scrollbar-width’,
${getScrollbarWidth()}px
);in css:
body.modal-opened { padding-right: calc(var(–scrollbar-width)); }
Just a side node – probably it’s better to call variable –vh100, as long it is “100vh”, not a single unit.
If you only need full-height elements, yes you could skip the
calc
part and set the variable to 100% of window.innerHeight. But if you need an element to be 50vh or else, you can use the variable and multiple it like so:height: calc(var(--vh, 1vh) * 50);
A really nice solution.
However there is a problem – if any script execution fails, JS fails to load or loading takes a long time, you’re going to have an unusable site.
Add a .js class to the body and make the calc height apply only when JS has loaded – The 100vh is both the fallback and non-js/slow loading version
There is already a fallback in the CSS in case the JavaScript doesn’t run.
In this line:
height: calc(var(--vh, 1vh) * 100);
there isvar(--vh, 1vh)
where1vh
is a fallback.This is not really mentioned in the article but CSS Custom Properties can have fallback if the property is not defined.
You can read more about this here: https://developer.mozilla.org/en-US/docs/Web/CSS/var
You could also add a default value on the root in your CSS.
Cheers ✌️
Hello,
I use the following code, never had an issue with 100vh not actually occupying the whole height.
This gets rid of the default behaviour. To me it looks like the issue you are having is caused by it.
After that IIjust manually add margins and padding as needed.
That’s very odd. Can you check this live demo on your mobile and let me know what you see?

Here is a screenshot from Chrome on Mobile. As you can see
100vh
refers to the screen without the interface.I got that working on chrome on my mobile device, but it is not working for Safari :(
Louis, please ignore my first comment, after looking into issue myself I have discovered more than I had hoped for. I always assumed that viewport height would be.. you know viewport height. Not the the mess it actually is.
So I have been researching a bit.. it appears that only solution that is somewhat reliable is the one you write about in your post maybe with some media queries… I am currently looking into it.
Meanwhile I made a little demo which seems to work fine, sort of… Ill try to use orientationchange event listener to handle the change of orientation and manually adjust height of pages which are below first 2 screen heights, because the URL bar will be always hidden at that point.
http://www.patriklegard.com/app
THANK YOU – this issue has irritated me for ages, and it seems obvious now but it actually never occurred to me to solve the problem this way using innerheight.
I wouldn’t recommend using the resize event though since the height of the element is then forced to change as you scroll on mobile(especially evident on safari ios). Meaning if there’s a background image on the element that is set to cover it makes the background position change, and will also affect any absolute positioned things inside that element too.
To avoid this issue I let the script only update my
vh
var when the resize is substantial enough(or in this case any landscape mode, mostly the desktop users)I am running into this issue on an aside with a sticky footer. I always want the footer to be visible since it contains the cancel and submit buttons but depending on scrolling it might show correcrly and it might not. Do you have any suggestions on a sticky footer in an aside on a mobile device?
Just in case somebody else runs into this issue, apparently in Chrome, window.innerHeight doesn’t return the correct viewport height if you’re in Device Mode in Dev Tools. I was trying to use this method on a personal website of mine but was stumped when I went into device mode to check how it looks on iOS and the console log showed a different value for innerHeight then the device viewport height. Firefox and Safari showed correct values but Chrome did not.
I found this article which seems to clarify the reason:
https://developers.google.com/web/updates/2017/09/visual-viewport-api
I needed to console log window.visualViewport.width if I wanted Chrome to use the visual viewport of the device. But if you’re actually on your mobile device innerHeight works fine, it’s just that when you’re emulating a mobile device on your laptop in Chrome dev tools innerHeight is not going to work as you may expect.
oops I meant window.visualViewport.height
Just nitpicking but might as well swap that
let
forconst
.