I'm currently developing a responsive site using Twitter Bootstrap.

The site has a full screen background image across mobile/tablet/desktop. These images rotate and fade through each, using two divs.

It's nearly perfect, except one issue. Using iOS Safari, Android Browser or Chrome on Android the background jumps slightly when a user scrolls down the page and causes the address bar to hide.

The site is here: http://lt2.daveclarke.me/

Visit it on a mobile device and scroll down and you should see the image resize/move.

The code I'm using for the background DIV is as follows:

#bg1 {
    background-color: #3d3d3f;
    background-repeat:no-repeat;
    background-attachment:fixed;
    background-position:center center;
    -webkit-background-size: cover;
    -moz-background-size: cover;
    -o-background-size: cover;
    background-size: cover; position:fixed;
    width:100%;
    height:100%;
    left:0px;
    top:0px;
    z-index:-1;
    display:none;
}

All suggestions welcome - this has been doing my head in for a while!!

Solution 1

This issue is caused by the URL bars shrinking/sliding out of the way and changing the size of the #bg1 and #bg2 divs since they are 100% height and "fixed". Since the background image is set to "cover" it will adjust the image size/position as the containing area is larger.

Based on the responsive nature of the site, the background must scale. I entertain two possible solutions:

1) Set the #bg1, #bg2 height to 100vh. In theory, this an elegant solution. However, iOS has a vh bug (http://thatemil.com/blog/2013/06/13/viewport-relative-unit-strangeness-in-ios-6/). I attempted using a max-height to prevent the issue, but it remained.

2) The viewport size, when determined by Javascript, is not affected by the URL bar. Therefore, Javascript can be used to set a static height on the #bg1 and #bg2 based on the viewport size. This is not the best solution as it isn't pure CSS and there is a slight image jump on page load. However, it is the only viable solution I see considering iOS's "vh" bugs (which do not appear to be fixed in iOS 7).

var bg = $("#bg1, #bg2");

function resizeBackground() {
    bg.height($(window).height());
}

$(window).resize(resizeBackground);
resizeBackground();

On a side note, I've seen so many issues with these resizing URL bars in iOS and Android. I understand the purpose, but they really need to think through the strange functionality and havoc they bring to websites. The latest change, is you can no longer "hide" the URL bar on page load on iOS or Chrome using scroll tricks.

EDIT: While the above script works perfectly for keeping the background from resizing, it causes a noticeable gap when users scroll down. This is because it is keeping the background sized to 100% of the screen height minus the URL bar. If we add 60px to the height, as swiss suggests, this problem goes away. It does mean we don't get to see the bottom 60px of the background image when the URL bar is present, but it prevents users from ever seeing a gap.

function resizeBackground() {
    bg.height( $(window).height() + 60);
}

Solution 2

I found that Jason's answer wasn't quite working for me and I was still getting a jump. The Javascript ensured there was no gap at the top of the page but the background was still jumping whenever the address bar disappeared/reappeared. So as well as the Javascript fix, I applied transition: height 999999s to the div. This creates a transition with a duration so long that it virtually freezes the element.

Solution 3

I've got a similar issue on a header of our website.

html, body {
    height:100%;
}
.header {
    height:100%;
}

This will end up in a jumpy scrolling experience on android chrome, because the .header-container will rescale after the url-bar hides and the finger is removed from screen.

CSS-Solution:

Adding the following two lines, will prevent that the url-bar hides and vertical scrolling is still possible:

html {
    overflow: hidden;
}
body {
    overflow-y: scroll;
    -webkit-overflow-scrolling:touch;
}

Solution 4

The problem can be solved with a media query and some math. Here's a solution for a portait orientation:

@media (max-device-aspect-ratio: 3/4) {
  height: calc(100vw * 1.333 - 9%);
}
@media (max-device-aspect-ratio: 2/3) {
  height: calc(100vw * 1.5 - 9%);
}
@media (max-device-aspect-ratio: 10/16) {
  height: calc(100vw * 1.6 - 9%);
}
@media (max-device-aspect-ratio: 9/16) {
  height: calc(100vw * 1.778 - 9%);
}

Since vh will change when the url bar dissapears, you need to determine the height another way. Thankfully, the width of the viewport is constant and mobile devices only come in a few different aspect ratios; if you can determine the width and the aspect ratio, a little math will give you the viewport height exactly as vh should work. Here's the process

1) Create a series of media queries for aspect ratios you want to target.

  • use device-aspect-ratio instead of aspect-ratio because the latter will resize when the url bar dissapears

  • I added 'max' to the device-aspect-ratio to target any aspect ratios that happen to follow in between the most popular. THey won't be as precise, but they will be only for a minority of users and will still be pretty close to the proper vh.

  • remember the media query using horizontal/vertical , so for portait you'll need to flip the numbers

2) for each media query multiply whatever percentage of vertical height you want the element to be in vw by the reverse of the aspect ratio.

  • Since you know the width and the ratio of width to height, you just multiply the % you want (100% in your case) by the ratio of height/width.

3) You have to determine the url bar height, and then minus that from the height. I haven't found exact measurements, but I use 9% for mobile devices in landscape and that seems to work fairly well.

This isn't a very elegant solution, but the other options aren't very good either, considering they are:

  • Having your website seem buggy to the user,

  • having improperly sized elements, or

  • Using javascript for some basic styling,

The drawback is some devices may have different url bar heights or aspect ratios than the most popular. However, using this method if only a small number of devices suffer the addition/subtraction of a few pixels, that seems much better to me than everyone having a website resize when swiping.

To make it easier, I also created a SASS mixin:

@mixin vh-fix {
  @media (max-device-aspect-ratio: 3/4) {
    height: calc(100vw * 1.333 - 9%);
  }
  @media (max-device-aspect-ratio: 2/3) {
    height: calc(100vw * 1.5 - 9%);
  }
  @media (max-device-aspect-ratio: 10/16) {
    height: calc(100vw * 1.6 - 9%);
  }
  @media (max-device-aspect-ratio: 9/16) {
    height: calc(100vw * 1.778 - 9%);
  }
}

Solution 5

All of the answers here are using window height, which is affected by the URL bar. Has everyone forgotten about screen height?

Here's my jQuery solution:

$(function(){

  var $w = $(window),
      $background = $('#background');

  // Fix background image jump on mobile
  if ((/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera)) {
    $background.css({'top': 'auto', 'bottom': 0});

    $w.resize(sizeBackground);
    sizeBackground();
  }

  function sizeBackground() {
     $background.height(screen.height);
  }
});

Adding the .css() part is changing the inevitably top-aligned absolute positioned element to bottom aligned, so there is no jump at all. Although, I suppose there's no reason not to just add that directly to the normal CSS.

We need the user agent sniffer because screen height on desktops would not be helpful.

Also, this is all assuming #background is a fixed-position element filling the window.

For the JavaScript purists (warning--untested):

var background = document.getElementById('background');

// Fix background image jump on mobile
if ((/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera)) {
  background.style.top = 'auto';
  background.style.bottom = 0;

  window.onresize = sizeBackground;
  sizeBackground();
}

function sizeBackground() {
  background.style.height = screen.height;
}

EDIT: Sorry that this does not directly answer your specific problem with more than one background. But this is one of the first results when searching for this problem of fixed backgrounds jumping on mobile.

Solution 6

I ran into this issue as well when I was trying to create an entrance screen that would cover the whole viewport. Unfortunately, the accepted answer no longer works.

1) Elements with the height set to 100vh get resized every time the viewport size changes, including those cases when it is caused by (dis)appearing URL bar.

2) $(window).height() returns values also affected by the size of the URL bar.

One solution is to "freeze" the element using transition: height 999999s as suggested in the answer by AlexKempton. The disadvantage is that this effectively disables adaptation to all viewport size changes, including those caused by screen rotation.

So my solution is to manage viewport changes manually using JavaScript. That enables me to ignore the small changes that are likely to be caused by the URL bar and react only on the big ones.

function greedyJumbotron() {
    var HEIGHT_CHANGE_TOLERANCE = 100; // Approximately URL bar height in Chrome on tablet

    var jumbotron = $(this);
    var viewportHeight = $(window).height();

    $(window).resize(function () {
        if (Math.abs(viewportHeight - $(window).height()) > HEIGHT_CHANGE_TOLERANCE) {
            viewportHeight = $(window).height();
            update();
        }
    });

    function update() {
        jumbotron.css('height', viewportHeight + 'px');
    }

    update();
}

$('.greedy-jumbotron').each(greedyJumbotron);

EDIT: I actually use this technique together with height: 100vh. The page is rendered properly from the very beginning and then the javascript kicks in and starts managing the height manually. This way there is no flickering at all while the page is loading (or even afterwards).

Solution 7

The solution I'm currently using is to check the userAgent on $(document).ready to see if it's one of the offending browsers. If it is, follow these steps:

  1. Set the relevant heights to the current viewport height rather than '100%'
  2. Store the current viewport horizontal value
  3. Then, on $(window).resize, only update the relevant height values if the new horizontal viewport dimension is different from it's initial value
  4. Store the new horizontal & vertical values

Optionally, you could also test permitting vertical resizes only when they are beyond the height of the address bar(s).

Oh, and the address bar does affect $(window).height. See: Mobile Webkit browser (chrome) address bar changes $(window).height(); making background-size:cover rescale every time JS "Window" width-height vs "screen" width-height?

Solution 8

I found a really easy solution without the use of Javascript:

transition: height 1000000s ease;
-webkit-transition: height 1000000s ease;
-moz-transition: height 1000000s ease;
-o-transition: height 1000000s ease;

All this does is delay the movement so that it's incredibly slow that it's not noticeable.

Solution 9

My solution involved a bit of javascript. Keep the 100% or 100vh on the div (this will avoid the div not appearing on initial page load). Then when the page loads, grab the window height and apply it to the element in question. Avoids the jump because now you have a static height on your div.

var $hero = $('#hero-wrapper'),
    h     = window.innerHeight;

$hero.css('height', h);

Solution 10

I created a vanilla javascript solution to using VH units. Using VH pretty much anywhere is effected by address bars minimizing on scroll. To fix the jank that shows when the page redraws, I've got this js here that will grab all your elements using VH units (if you give them the class .vh-fix), and give them inlined pixel heights. Essentially freezing them at the height we want. You could do this on rotation or on viewport size change to stay responsive.

var els = document.querySelectorAll('.vh-fix')
if (!els.length) return

for (var i = 0; i < els.length; i++) {
  var el = els[i]
  if (el.nodeName === 'IMG') {
    el.onload = function() {
      this.style.height = this.clientHeight + 'px'
    }
  } else {
    el.style.height = el.clientHeight + 'px'
  }
}

This has solved all my use cases, hope it helps.

Solution 11

this is my solution to resolve this issue, i have added comments directly on code. I've tested this solution and works fine. Hope we will be useful for everyone has the same problem.

//remove height property from file css because we will set it to first run of page
//insert this snippet in a script after declarations of your scripts in your index

var setHeight = function() {    
  var h = $(window).height();   
  $('#bg1, #bg2').css('height', h);
};

setHeight(); // at first run of webpage we set css height with a fixed value

if(typeof window.orientation !== 'undefined') { // this is more smart to detect mobile devices because desktop doesn't support this property
  var query = window.matchMedia("(orientation:landscape)"); //this is is important to verify if we put 
  var changeHeight = function(query) {                      //mobile device in landscape mode
    if (query.matches) {                                    // if yes we set again height to occupy 100%
      setHeight(); // landscape mode
    } else {                                                //if we go back to portrait we set again
      setHeight(); // portrait mode
    }
  }
  query.addListener(changeHeight);                          //add a listner too this event
} 
else { //desktop mode                                       //this last part is only for non mobile
  $( window ).resize(function() {                           // so in this case we use resize to have
    setHeight();                                            // responsivity resisizing browser window
  }); 
};

Solution 12

This is the best solution (simplest) above everything I have tried.

...And this does not keep the native experience of the address bar!

You could set the height with CSS to 100% for example, and then set the height to 0.9 * of the window height in px with javascript, when the document is loaded.

For example with jQuery:

$("#element").css("height", 0.9*$(window).height());

)

Unfortunately there isn't anything that works with pure CSS :P but also be minfull that vh and vw are buggy on iPhones - use percentages.

Solution 13

I set my width element, with javascript. After I set the de vh.

html

<div id="gifimage"><img src="back_phone_back.png" style="width: 100%"></div>

css

#gifimage {
    position: absolute;
    height: 70vh;
}

javascript

imageHeight = document.getElementById('gifimage').clientHeight;

// if (isMobile)       
document.getElementById('gifimage').setAttribute("style","height:" + imageHeight + "px");

This works for me. I don't use jquery ect. because I want it to load as soon as posible.

Solution 14

It took a few hours to understand all the details of this problem and figure out the best implementation. It turns out as of April 2016 only iOS Chrome has this problem. I tried on Android Chrome and iOS Safari-- both were fine without this fix. So the fix for iOS Chrome I'm using is:

$(document).ready(function() {
   var screenHeight = $(window).height();
   $('div-with-background-image').css('height', screenHeight + 'px');
});

Solution 15

Similar to @AlexKempton answer. (couldn't comment sorry)

I've been using long transition delays to prevent the element resizing.

eg:

transition: height 250ms 600s; /*10min delay*/

The tradeoff with this is it prevents resizing of the element including on device rotating, however, you could use some JS to detect orientationchange and reset the height if this was an issue.

Solution 16

Simple answer if your background allows, why not set background-size: to something just covering device width with media queries and use alongside the :after position: fixed; hack here.

IE: background-size: 901px; for any screens <900px? Not perfect or responsive but worked a charm for me on mobile <480px as i'm using an abstract BG.

Solution 17

My answer is for everyone who comes here (like I did) to find an answer for a bug caused by the hiding address bare / browser interface.

The hiding address bar causes the resize-event to trigger. But different than other resize-events, like switching to landscape mode, this doesn't change the width of the window. So my solution is to hook into the resize event and check if the width is the same.

// Keep track of window width
let myWindowWidth = window.innerWidth;

window.addEventListener( 'resize', function(event) {

    // If width is the same, assume hiding address bar
    if( myWindowWidth == window.innerWidth ) {
         return;
    }

    // Update the window width
    myWindowWidth = window.innerWidth;

    // Do your thing
    // ...
});

Solution 18

I am using this workaround: Fix bg1's height on page load by:

var BG = document.getElementById('bg1');
BG.style.height = BG.parentElement.clientHeight;

Then attach a resize event listener to Window which does this: if difference in height after resizing is less than 60px (anything more than url bar height) then do nothing and if it is greater than 60px then set bg1's height again to its parent's height! complete code:

window.addEventListener("resize", onResize, false);
var BG = document.getElementById('bg1');
BG.style.height = BG.parentElement.clientHeight;
var oldH = BG.parentElement.clientHeight;

function onResize() {
    if(Math.abs(oldH - BG.parentElement.clientHeight) > 60){
      BG.style.height = BG.parentElement.clientHeight + 'px';
      oldH = BG.parentElement.clientHeight;
    }
}

PS: This bug is so irritating!

Solution 19

For those who would like to listen to the actual inner height and vertical scroll of the window while the Chrome mobile browser is transition the URL bar from shown to hidden and vice versa, the only solution that I found is to set an interval function, and measure the discrepancy of the window.innerHeight with its previous value.

This introduces this code:

var innerHeight = window.innerHeight;
window.setInterval(function ()
{
  var newInnerHeight = window.innerHeight;
  if (newInnerHeight !== innerHeight)
  {
    var newScrollY = window.scrollY + newInnerHeight - innerHeight;
    // ... do whatever you want with this new scrollY
    innerHeight = newInnerHeight;
  }
}, 1000 / 60);

I hope that this will be handy. Does anyone knows a better solution?

Solution 20

The solution I came up with when having similar problem was to set height of element to window.innerHeight every time touchmove event was fired.

var lastHeight = '';

$(window).on('resize', function () {
    // remove height when normal resize event is fired and content redrawn
    if (lastHeight) {
        $('#bg1').height(lastHeight = '');
    }
}).on('touchmove', function () {
    // when window height changes adjust height of the div
    if (lastHeight != window.innerHeight) {
        $('#bg1').height(lastHeight = window.innerHeight);
    }
});

This makes the element span exactly 100% of the available screen at all times, no more and no less. Even during address bar hiding or showing.

Nevertheless it's a pity that we have to come up with that kind of patches, where simple position: fixed should work.

Solution 21

With the support of CSS custom properties (variables) in iOS, you can set these with JS and use them on iOS only.

const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
if (iOS) {
  document.body.classList.add('ios');
  const vh = window.innerHeight / 100;
  document.documentElement.style
    .setProperty('--ios-10-vh', `${10 * vh}px`);
  document.documentElement.style
    .setProperty('--ios-50-vh', `${50 * vh}px`);
  document.documentElement.style
    .setProperty('--ios-100-vh', `${100 * vh}px`);
}
body.ios {
    .side-nav {
        top: var(--ios-50-vh);
    }
    section {
        min-height: var(--ios-100-vh);
        .container {
            position: relative;
            padding-top: var(--ios-10-vh);
            padding-bottom: var(--ios-10-vh);
        }
    }
}