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!!
This question is related to
android
html
ios
css
twitter-bootstrap-3
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.
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%);
}
}
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).
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
});
};
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;
}
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.
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.
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.
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);
}
}
}
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.
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.
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.
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.
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);
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
// ...
});
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.
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!
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:
$(window).resize
, only update the relevant height values if the new horizontal viewport dimension is different from it's initial value 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?
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;_x000D_
window.setInterval(function ()_x000D_
{_x000D_
var newInnerHeight = window.innerHeight;_x000D_
if (newInnerHeight !== innerHeight)_x000D_
{_x000D_
var newScrollY = window.scrollY + newInnerHeight - innerHeight;_x000D_
// ... do whatever you want with this new scrollY_x000D_
innerHeight = newInnerHeight;_x000D_
}_x000D_
}, 1000 / 60);
_x000D_
I hope that this will be handy. Does anyone knows a better solution?
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.
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');
});
Source: Stackoverflow.com