How do I reset the scale/zoom of a web app on an orientation change on the iPhone?

JavascriptIphoneIpadOrientation

Javascript Problem Overview


When I start my app in portrait mode, it works fine. Then I rotate into landscape and it's scaled up. To get it to scale correctly for the landscape mode I have to double tap on something twice, first to zoom all the way in (the normal double tap behavior) and again to zoom all the way out (again, the normal double tap behavior). When it zooms out, it zooms out to the correct NEW scale for landscape mode.

Switching back to portrait seems to work more consistently; that is, it handles the zoom so that the scale is correct when the orientation changes back to portrait.

I am trying to figure out if this is a bug? or if this is something that can be fixed with JavaScript?

With the viewport meta content, I am setting the initial-scale to 1.0 and I am NOT setting minimum or maximum scale (nor do I want to). I am setting the width to device-width.

Any ideas? I know a lot of people would be grateful to have a solution as it seems to be a persistent problem.

Javascript Solutions


Solution 1 - Javascript

Jeremy Keith (@adactio) has a good solution for this on his blog Orientation and scale

Keep the Markup scalable by not setting a maximum-scale in markup.

<meta name="viewport" content="width=device-width, initial-scale=1">

Then disable scalability with javascript on load until gesturestart when you allow scalability again with this script:

if (navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPad/i)) {
	var viewportmeta = document.querySelector('meta[name="viewport"]');
	if (viewportmeta) {
		viewportmeta.content = 'width=device-width, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0';
		document.body.addEventListener('gesturestart', function () {
			viewportmeta.content = 'width=device-width, minimum-scale=0.25, maximum-scale=1.6';
		}, false);
	}
}

Update 22-12-2014:
On an iPad 1 this doesnt work, it fails on the eventlistener. I've found that removing .body fixes that:

document.addEventListener('gesturestart', function() { /* */ });

Solution 2 - Javascript

Scott Jehl came up with a fantastic solution that uses the accelerometer to anticipate orientation changes. This solution is very responsive and does not interfere with zoom gestures.

https://github.com/scottjehl/iOS-Orientationchange-Fix

> How it works: This fix works by listening to the device's > accelerometer to predict when an orientation change is about to occur. > When it deems an orientation change imminent, the script disables user > zooming, allowing the orientation change to occur properly, with > zooming disabled. The script restores zoom again once the device is > either oriented close to upright, or after its orientation has > changed. This way, user zooming is never disabled while the page is in > use.

Minified source:

/*! A fix for the iOS orientationchange zoom bug. Script by @scottjehl, rebound by @wilto.MIT License.*/(function(m){if(!(/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1)){return}var l=m.document;if(!l.querySelector){return}var n=l.querySelector("meta[name=viewport]"),a=n&&n.getAttribute("content"),k=a+",maximum-scale=1",d=a+",maximum-scale=10",g=true,j,i,h,c;if(!n){return}function f(){n.setAttribute("content",d);g=true}function b(){n.setAttribute("content",k);g=false}function e(o){c=o.accelerationIncludingGravity;j=Math.abs(c.x);i=Math.abs(c.y);h=Math.abs(c.z);if(!m.orientation&&(j>7||((h>6&&i<8||h<8&&i>6)&&j>5))){if(g){b()}}else{if(!g){f()}}}m.addEventListener("orientationchange",f,false);m.addEventListener("devicemotion",e,false)})(this);

Solution 3 - Javascript

I had the same problem, and setting the maximum-scale=1.0 worked for me.

Edit: As mentioned in the comments this does disable user zoom except when the content exceeds the width-resolution. As mentioned, this might not be wise. It might also be desired in some cases.

The viewport code:

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0;">

Solution 4 - Javascript

If you have the width set in the viewport :

<meta name = "viewport" content = "width=device-width; initial-scale=1.0;
 maximum-scale=1.0;" />

And then change the orientation it will randomly zoom in sometimes (especially if you are dragging on the screen) to fix this don't set a width here I used :

<meta id="viewport" name="viewport" content="initial-scale=1.0; user-scalable=0;
minimum-scale=1.0; maximum-scale=1.0" />

This fixes the zoom whatever happens then you can use either window.onorientationchange event or if you want it to be platform independant (handy for testing) the window.innerWidth method.

Solution 5 - Javascript

MobileSafari supports the orientationchange event on the window object. Unfortunately there doesn't seem to be a way to directly control the zoom via JavaScript. Perhaps you could dynamically write/change the meta tag which controls the viewport — but I doubt that would work, it only affects the initial state of the page. Perhaps you could use this event to actually resize your content using CSS. Good luck!

Solution 6 - Javascript

I created a working demo of a landscape/portrait layout but the zoom must be disabled for it to work without JavaScript:

http://matthewjamestaylor.com/blog/ipad-layout-with-landscape-portrait-modes

Solution 7 - Javascript

I've been using this function in my project.

function changeViewPort(key, val) {
	var reg = new RegExp(key, "i"), oldval = document.querySelector('meta[name="viewport"]').content;
	var newval = reg.test(oldval) ? oldval.split(/,\s*/).map(function(v){ return reg.test(v) ? key+"="+val : v; }).join(", ") : oldval+= ", "+key+"="+val ;
	document.querySelector('meta[name="viewport"]').content = newval;
}

so just addEventListener:

if( /iPad|iPhone|iPod|Android/i.test(navigator.userAgent) ){
    window.addEventListener("orientationchange", function() { 
        changeViewPort("maximum-scale", 1);
        changeViewPort("maximum-scale", 10);
    }
}

Solution 8 - Javascript

Elisabeth you can change viewport content dynamically by adding the "id" property to the metatag:

<meta name="viewport" id="view" content="user-scalable=yes, width=device-width minimum-scale=1, maximum-scale=1" />

Then you just can call by javascript:

document.getElementById("view").setAttribute('content','user-scalable=yes, width=device-width, minimum-scale=1, maximum-scale=10');

Solution 9 - Javascript

I have found a new workaround, different from any other that I have seen, by disabling the native iOS zoom, and instead implementing zoom functionality in JavaScript.

An excellent background on the various other solutions to the zoom/orientation problem is by Sérgio Lopes: A fix to the famous iOS zoom bug on orientation change to portrait.

<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<meta name="viewport" id="viewport" content="user-scalable=no,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0" />
	<title>Robocat mobile Safari zoom fix</title>
	<style>
		body {
			padding: 0;
			margin: 0;
		}
		#container {
			-webkit-transform-origin: 0px 0px;
			-webkit-transform: scale3d(1,1,1);
			/* shrink-to-fit needed so can measure width of container http://stackoverflow.com/questions/450903/make-css-div-width-equal-to-contents */
			display: inline-block;
			*display: inline;
			*zoom: 1;
		}
		#zoomfix {
			opacity: 0;
			position: absolute;
			z-index: -1;
			top: 0;
			left: 0;
		}
	</style>
</head>

<body>
	<input id="zoomfix" disabled="1" tabIndex="-1">
	<div id="container">
		<style>
			table {
				counter-reset: row cell;
				background-image: url(http://upload.wikimedia.org/wikipedia/commons/3/38/JPEG_example_JPG_RIP_010.jpg);
			}
			tr {
				counter-increment: row;
			}
			td:before {
				counter-increment: cell;
				color: white;
				font-weight: bold;
				content: "row" counter(row) ".cell" counter(cell);
			}
		</style>
		<table cellspacing="10">
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
			<tr><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td><td>
		</table>
	</div>

	<script>
	(function() {
		var viewportScale = 1;
		var container = document.getElementById('container');
		var scale, originX, originY, relativeOriginX, relativeOriginY, windowW, windowH, containerW, containerH, resizeTimer, activeElement;
		document.addEventListener('gesturestart', function(event) {
			scale = null;
			originX = event.pageX;
			originY = event.pageY;
			relativeOriginX = (originX - window.pageXOffset) / window.innerWidth;
			relativeOriginY = (originY - window.pageYOffset) / window.innerHeight;
			windowW = window.innerWidth;
			windowH = window.innerHeight;
			containerW = container.offsetWidth;
			containerH = container.offsetHeight;
		});
		document.addEventListener('gesturechange', function(event) {
			event.preventDefault();
			if (originX && originY && event.scale && event.pageX && event.pageY) {
				scale = event.scale;
				var newWindowW = windowW / scale;
				if (newWindowW > containerW) {
					scale = windowW / containerW;
				}
				var newWindowH = windowH / scale;
				if (newWindowH > containerH) {
					scale = windowH / containerH;
				}
				if (viewportScale * scale < 0.1) {
					scale = 0.1/viewportScale;
				}
				if (viewportScale * scale > 10) {
					scale = 10/viewportScale;
				}
				container.style.WebkitTransformOrigin = originX + 'px ' + originY + 'px';
				container.style.WebkitTransform = 'scale3d(' + scale + ',' + scale + ',1)';
			}
		});
		document.addEventListener('gestureend', function() {
			if (scale && (scale < 0.95 || scale > 1.05)) {
				viewportScale *= scale;
				scale = null;
				container.style.WebkitTransform = '';
				container.style.WebkitTransformOrigin = '';
				document.getElementById('viewport').setAttribute('content', 'user-scalable=no,initial-scale=' + viewportScale + ',minimum-scale=' + viewportScale + ',maximum-scale=' + viewportScale);
				document.body.style.WebkitTransform = 'scale3d(1,1,1)';
				// Without zoomfix focus, after changing orientation and zoom a few times, the iOS viewport scale functionality sometimes locks up (and completely stops working).
				// The reason I thought this hack would work is because showing the keyboard is the only way to affect the viewport sizing, which forces the viewport to resize (even though the keyboard doesn't actually get time to open!).
				// Also discovered another amazing side effect: if you have no meta viewport element, and focus()/blur() in gestureend, zoom is disabled!! Wow!
				var zoomfix = document.getElementById('zoomfix');
				zoomfix.disabled = false;
				zoomfix.focus();
				zoomfix.blur();
				setTimeout(function() {
					zoomfix.disabled = true;
					window.scrollTo(originX - relativeOriginX * window.innerWidth, originY - relativeOriginY * window.innerHeight);
					// This forces a repaint. repaint *intermittently* fails to redraw correctly, and this fixes the problem.
					document.body.style.WebkitTransform = '';
				}, 0);
			}
		});
	})();
	</script>
</body>
</html>

It could be improved, but for my needs it avoids the major drawbacks that occur with all the other solutions I have seen. So far I have only tested it using mobile Safari on an iPad 2 with iOS4.

The focus()/blur() is a workaround to prevent the occasional lockup of the zoom functionality which can occur after changing orientation and zooming a few times.

Setting the document.body.style forces a full screen repaint, which avoids an occasional intermittent problems where the repaint badly fails after zoom.

Solution 10 - Javascript

Found a very easily implemented fix. Set the focus to a text element that has a font size of 50px on completion of the form. It does not seem to work if the text element is hidden but hiding this element is easily done by setting the elements color properties to have no opacity.

Solution 11 - Javascript

Here's another way to do it, which seems to work well.

  1. Set the meta tag to restrict the viewport to scale=1, which prevents zooming:

    < meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">

  2. With javascript, change the meta tag 1/2 second later to allow zooming:

    setTimeout(function(){ document.querySelector("meta[name=viewport]").setAttribute('content','width=device-width, initial-scale=1');}, 500);

  3. Again with javascript, on orientation change, reload the page:

    window.onorientationchange = function(){window.location.reload();};

Every time you reorient the device, the page reloads, initially without zoom. But 1/2 second later, ability to zoom is restored.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionElisabethView Question on Stackoverflow
Solution 1 - JavascriptsnobojohanView Answer on Stackoverflow
Solution 2 - JavascriptAndrew AshbacherView Answer on Stackoverflow
Solution 3 - JavascriptrakaloofView Answer on Stackoverflow
Solution 4 - JavascriptpsyderView Answer on Stackoverflow
Solution 5 - JavascriptAvi FlaxView Answer on Stackoverflow
Solution 6 - JavascriptMatthew James TaylorView Answer on Stackoverflow
Solution 7 - JavascriptJames YangView Answer on Stackoverflow
Solution 8 - JavascriptM PenadesView Answer on Stackoverflow
Solution 9 - JavascriptrobocatView Answer on Stackoverflow
Solution 10 - JavascriptDellsmashView Answer on Stackoverflow
Solution 11 - JavascriptMark LampreyView Answer on Stackoverflow