[html] CSS3 100vh not constant in mobile browser

I have a very odd issue... in every browser and mobile version I encountered this behavior:

  • all the browsers have a top menu when you load the page (showing the address bar for example) which slide up when you start scrolling the page.
  • 100vh sometimes is calculated only on the visible part of a viewport, so when the browser bar slide up 100vh increases (in terms of pixels)
  • all layout re-paint and re-adjust since the dimensions have changed
  • a bad jumpy effect for user experience

How can avoid this problem? When I first heard of viewport-height I was excited and I thought I could use it for fixed height blocks instead of using javascript, but now I think the only way to do that is in fact javascript with some resize event...

you can see the problem at: sample site

Can anyone help me with / suggest a CSS solution?


simple test code:

_x000D_
_x000D_
/* maybe i can track the issue whe it occours... */_x000D_
$(function(){_x000D_
  var resized = -1;_x000D_
  $(window).resize(function(){_x000D_
    $('#currenth').val( $('.vhbox').eq(1).height() );_x000D_
    if (++resized) $('#currenth').css('background:#00c');_x000D_
  })_x000D_
  .resize();_x000D_
})
_x000D_
*{ margin:0; padding:0; }_x000D_
_x000D_
/*_x000D_
  this is the box which should keep constant the height..._x000D_
  min-height to allow content to be taller than viewport if too much text_x000D_
*/_x000D_
.vhbox{_x000D_
  min-height:100vh;_x000D_
  position:relative;_x000D_
}_x000D_
_x000D_
.vhbox .t{_x000D_
  display:table;_x000D_
  position:relative;_x000D_
  width:100%;_x000D_
  height:100vh;_x000D_
}_x000D_
_x000D_
.vhbox .c{_x000D_
  height:100%;_x000D_
  display:table-cell;_x000D_
  vertical-align:middle;_x000D_
  text-align:center;_x000D_
}
_x000D_
<div class="vhbox" style="background-color:#c00">_x000D_
  <div class="t"><div class="c">_x000D_
  this div height should be 100% of viewport and keep this height when scrolling page_x000D_
    <br>_x000D_
    <!-- this input highlight if resize event is fired -->_x000D_
    <input type="text" id="currenth">_x000D_
  </div></div>_x000D_
</div>_x000D_
_x000D_
<div class="vhbox" style="background-color:#0c0">_x000D_
  <div class="t"><div class="c">_x000D_
  this div height should be 100% of viewport and keep this height when scrolling page_x000D_
  </div></div>_x000D_
</div>_x000D_
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
_x000D_
_x000D_
_x000D_

This question is related to html css viewport-units

The answer is


The following code solved the problem (with jQuery).

var vhHeight = $("body").height();
var chromeNavbarHeight = vhHeight - window.innerHeight;
$('body').css({ height: window.innerHeight, marginTop: chromeNavbarHeight });

And the other elements use % as a unit to replace vh.


The following worked for me:

html { height: 100vh; }

body {
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100vw;
}

/* this is the container you want to take the visible viewport  */
/* make sure this is top-level in body */
#your-app-container {
  height: 100%;
}

The body will take the visible viewport height and #your-app-container with height: 100% will make that container take the visible viewport height.


Try html, body { height: 100% } for something to the effect of 100vh on mobile devices.


Hopefully, this will be a UA-defined CSS environment variable as suggested here: https://github.com/w3c/csswg-drafts/issues/2630#issuecomment-397536046


Look at this answer: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/

_x000D_
_x000D_
// First we get the viewport height and we multiple it by 1% to get a value for a vh unit_x000D_
let vh = window.innerHeight * 0.01;_x000D_
// Then we set the value in the --vh custom property to the root of the document_x000D_
document.documentElement.style.setProperty('--vh', `${vh}px`);_x000D_
_x000D_
// We listen to the resize event_x000D_
window.addEventListener('resize', () => {_x000D_
  // We execute the same script as before_x000D_
  let vh = window.innerHeight * 0.01;_x000D_
  document.documentElement.style.setProperty('--vh', `${vh}px`);_x000D_
});
_x000D_
body {_x000D_
  background-color: #333;_x000D_
}_x000D_
_x000D_
.module {_x000D_
  height: 100vh; /* Use vh as a fallback for browsers that do not support Custom Properties */_x000D_
  height: calc(var(--vh, 1vh) * 100);_x000D_
  margin: 0 auto;_x000D_
  max-width: 30%;_x000D_
}_x000D_
_x000D_
.module__item {_x000D_
  align-items: center;_x000D_
  display: flex;_x000D_
  height: 20%;_x000D_
  justify-content: center;_x000D_
}_x000D_
_x000D_
.module__item:nth-child(odd) {_x000D_
  background-color: #fff;_x000D_
  color: #F73859;_x000D_
}_x000D_
_x000D_
.module__item:nth-child(even) {_x000D_
  background-color: #F73859;_x000D_
  color: #F1D08A;_x000D_
}
_x000D_
<div class="module">_x000D_
  <div class="module__item">20%</div>_x000D_
  <div class="module__item">40%</div>_x000D_
  <div class="module__item">60%</div>_x000D_
  <div class="module__item">80%</div>_x000D_
  <div class="module__item">100%</div>_x000D_
</div>
_x000D_
_x000D_
_x000D_


You can try min-height: -webkit-fill-available; in your css instead of 100vh. It should be solved


Because it won't be fixed, you can do something like:

# html
<body>
  <div class="content">
    <!-- Your stuff here -->
  </div>
</body>

# css
.content {
  height: 80vh;
}

For me it was the fastest and more pure solution than playing with the JavaScript which could not work on many devices and browsers.

Just use proper value of vh which fits your needs.


I came up with a React component – check it out if you use React or browse the source code if you don't, so you can adapt it to your environment.

It sets the fullscreen div's height to window.innerHeight and then updates it on window resizes.


A nice read about the problem and its possible solutions can be found in this blog post: Addressing the iOS Address Bar in 100vh Layouts

The solution I ended up in my React application is utilising the react-div-100vh library described in the post above.


I just found a web app i designed has this issue with iPhones and iPads, and found an article suggesting to solve it using media queries targeted at specific Apple devices.

I don't know whether I can share the code from that article here, but the address is this: http://webdesignerwall.com/tutorials/css-fix-for-ios-vh-unit-bug

Quoting the article: "just match the element height with the device height using media queries that targets the older versions of iPhone and iPad resolution."

They added just 6 media queries to adapt full height elements, and it should work as it is fully CSS implemented.

Edit pending: I'm unable to test it right now, but I will come back and report my results.


You can do this by adding following script and style

  function appHeight() {
    const doc = document.documentElement
    doc.style.setProperty('--vh', (window.innerHeight*.01) + 'px');
  }

  window.addEventListener('resize', appHeight);
  appHeight();

Style

.module {
  height: 100vh; /* Fallback for browsers that do not support Custom Properties */
  height: calc(var(--vh, 1vh) * 100);
}

As I am new, I can't comment on other answers.

If someone is looking for an answer to make this work (and can use javascript - as it seems to be required to make this work at the moment) this approach has worked pretty well for me and it accounts for mobile orientation change as well. I use Jquery for the example code but should be doable with vanillaJS.

-First, I use a script to detect if the device is touch or hover. Bare-bones example:

if ("ontouchstart" in document.documentElement) {
    document.body.classList.add('touch-device');

} else {
    document.body.classList.add('hover-device');
}

This adds class to the body element according to the device type (hover or touch) that can be used later for the height script.

-Next use this code to set height of the device on load and on orientation change:

if (jQuery('body').hasClass("touch-device")) {
//Loading height on touch-device
    function calcFullHeight() {
        jQuery('.hero-section').css("height", $(window).height());
    }

    (function($) {
        calcFullHeight();

        jQuery(window).on('orientationchange', function() {
            // 500ms timeout for getting the correct height after orientation change
            setTimeout(function() {
                calcFullHeight();
            }, 500);

        });
    })(jQuery);

} else {
    jQuery('.hero-section').css("height", "100vh");


}

-Timeout is set so that the device would calculate the new height correctly on orientation change. If there is no timeout, in my experience the height will not be correct. 500ms might be an overdo but has worked for me.

-100vh on hover-devices is a fallback if the browser overrides the CSS 100vh.


Here's a work around I used for my React app.

iPhone 11 Pro & iPhone Pro Max - 120px

iPhone 8 - 80px

max-height: calc(100vh - 120px);

It's a compromise but relatively simple fix


@nils explained it clearly.

What's next then?

I just went back to use relative 'classic' % (percentage) in CSS.

It's often more effort to implement something than it would be using vh, but at least, you have a pretty stable solution which works across different devices and browsers without strange UI glitches.


As I was looking for a solution some days, here is mine for everyone using VueJS with Vuetify (my solution uses v-app-bar, v-navigation-drawer and v-footer): I created App.scss (used in App.vue) with the following content:

_x000D_
_x000D_
.v-application {_x000D_
    height: 100vh;_x000D_
    height: -webkit-fill-available;_x000D_
}_x000D_
_x000D_
.v-application--wrap {_x000D_
    min-height: 100vh !important;_x000D_
    min-height: -webkit-fill-available !important;_x000D_
}
_x000D_
_x000D_
_x000D_


The VH 100 does not work well on mobile as it does not factor in the iOS bar (or similar functionality on other platforms).

One solution that works well is to use JavaScript "window.innerHeight".

Simply assign the height of the element to this value e.g. $('.element-name').height(window.innerHeight);

Note: It may be useful to create a function in JS, so that the height can change when the screen is resized. However, I would suggest only calling the function when the width of the screen is changed, this way the element will not jump in height when the iOS bar disappears when the user scrolls down the page.


Using vh on mobile devices is not going to work with 100vh, due to their design choices using the entire height of the device not including any address bars etc.

If you are looking for a layout including div heights proportionate to the true view height I use the following pure css solution:

:root {
  --devHeight: 86vh; //*This value changes
}

.div{
    height: calc(var(--devHeight)*0.10); //change multiplier to suit required height
}

You have two options for setting the viewport height, manually set the --devHeight to a height that works (but you will need to enter this value for each type of device you are coding for)

or

Use javascript to get the window height and then update --devheight on loading and refreshing the viewport (however this does require using javascript and is not a pure css solution)

Once you obtain your correct view height you can create multiple divs at an exact percentage of total viewport height by simply changing the multiplier in each div you assign the height to.

0.10 = 10% of view height 0.57 = 57% of view height

Hope this might help someone ;)


For many of the sites I build the client will ask for a 100vh banner and just as you have found, it results in a bad "jumpy" experience on mobile when you begin to scroll. This is how I solve the problem for a smooth consistent experience across all devices:

I first set my banner element CSS to height:100vh

Then I use jQuery to get the height in pixels of my banner element and apply an inline style using this height.

var viewportHeight = $('.banner').outerHeight();
$('.banner').css({ height: viewportHeight });

Doing this solves the issue on mobile devices as when the page loads, the banner element is set to 100vh using CSS and then jQuery overrides this by putting inline CSS on my banner element which stops it from resizing when a user begins to scroll.

However, on desktop if a user resizes their browser window my banner element won't resize because it now has a fixed height set in pixels due to the above jQuery. To address this I use Mobile Detect to add a 'mobile' class to the body of my document. And then I wrap the above jQuery in an if statement:

if ($('body').hasClass('mobile')) {
  var viewportHeight = $('.banner').outerHeight();
  $('.banner').css({ height: viewportHeight });
}

As a result, if a user is on a mobile device the class 'mobile' is present on the body of my page and the above jQuery is executed. So my banner element will only get the inline CSS applied on mobile devices meanwhile on desktop the original 100vh CSS rule remains in place.


Brave browser on iOS behaves differently (buggy?). It changes viewport height dynamically accordingly to showing/hiding address bar. It is kind of annoying because it changes page's layout dependent on vw/vh units.

Chrome and Safari is fine.


in my app I do it like so (typescript and nested postcss, so change the code accordingly):

const appHeight = () => {
    const doc = document.documentElement
    doc.style.setProperty('--app-height', `${window.innerHeight}px`)
}
window.addEventListener('resize', appHeight)
appHeight()

in your css:

:root {
   --app-height: 100%;
}

html,
body {
    padding: 0;
    margin: 0;
    overflow: hidden;
    width: 100vw;
    height: 100vh;

    @media not all and (hover:hover) {
        height: var(--app-height);
    }
}

it works at least on chrome mobile and ipad. What doesn't work is when you add your app to homescreen on iOS and change the orientation a few times - somehow the zoom levels mess with the innerHeight value, I might post an update if I find a solution to it.

Demo


For me such trick made a job:

height: calc(100vh - calc(100vh - 100%))

You can try giving position: fixed; top: 0; bottom: 0; properties to your container.