Paul Irish, Divya Manian and Shi Chuan launched Mobile Boilerplate recently—a mobile companion site to HTML5 Boilerplate.
There’s some good stuff in there but I was a little surprised to see that the meta viewport
element included values for minimum-scale=1.0, maximum-scale=1.0, user-scalable=no
:
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
Setting user-scalable=no
is pretty much the same as setting minimum-scale=1.0, maximum-scale=1.0
. In any case, I’m not keen on it. Like Roger, I don’t think we should take away the user’s right to pinch and zoom to make content larger. That’s why my usual viewport declaration is:
<meta name="viewport" content="width=device-width, initial-scale=1">
Yes, I know that most native apps don’t allow you to zoom but I see no reason to replicate that failing on the web.
But there’s a problem. Allowing users to scale content for comfort would be fine if it weren’t for a bug in Mobile Safari:
When the meta viewport tag is set to content="width=device-width,initial-scale=1"
, or any value that allows user-scaling, changing the device to landscape orientation causes the page to scale larger than 1.0. As a result, a portion of the page is cropped off the right, and the user must double-tap (sometimes more than once) to get the page to zoom properly into view.
This is really annoying so Shi Chuan set about fixing the problem.
His initial solution was to keep minimum-scale=1.0, maximum-scale=1.0
in the meta viewport
element but then to change it using JavaScript once the user initiates a gesture (the gesturestart
event is triggered as soon as two fingers are on the screen). At the point, the content
attribute of the meta viewport
element gets updated to read minimum-scale=0.25, maximum-scale=1.6
, the default values:
var metas = document.getElementsByTagName('meta');
var i;
if (navigator.userAgent.match(/iPhone/i)) {
document.addEventListener("gesturestart", gestureStart, false);
function gestureStart() {
for (i=0; i<metas.length; i++) {
if (metas[i].name == "viewport") {
metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6";
}
}
}
}
That works nicely but I wasn’t too keen on the dependency between the markup and the script. If, for whatever reason, the script doesn’t get executed, users are stuck with an unzoomable page.
I suggested that the script should also set the initial value to minimum-scale=1.0, maximum-scale=1.0
:
var metas = document.getElementsByTagName('meta');
var i;
if (navigator.userAgent.match(/iPhone/i)) {
for (i=0; i<metas.length; i++) {
if (metas[i].name == "viewport") {
metas[i].content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0";
}
}
document.addEventListener("gesturestart", gestureStart, false);
}
function gestureStart() {
for (i=0; i<metas.length; i++) {
if (metas[i].name == "viewport") {
metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6";
}
}
}
Now the markup still contains the robust accessible default:
<meta name="viewport" content="width=device-width, initial-scale=1">
…while the script takes care of initially setting the scale values and also updating them when a gesture is detected. Here’s what’s happening:
- By default, the page is scaleable because the initial
meta viewport
declaration doesn’t set a minimum-scale
or maximum-scale
.
- Once the script loads, the page is no longer scalable because both
minimum-scale
and maximum-scale
have been set to 1.0
. If the device is switched from portrait to landscape, the resizing bug won’t be triggered because scaling is disabled.
- When the
gesturestart
event is detected—indicating that the user might be trying to scale the page—the minimum-scale
and maximum-scale
values are updated to allow scaling. At this point, if the device is switched from portrait to landscape, the resizing bug will occur because the page is now scaleable.
Jason Weaver points out that you should probably detect for iPad too. That’s a pretty straightforward update:
if (navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPad/i))
Mathias Bynens updated the code to use querySelectorAll
which is supported in Mobile Safari. Here’s the code I’m currently using:
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';
document.body.addEventListener('gesturestart', function() {
viewportmeta.content = 'width=device-width, minimum-scale=0.25, maximum-scale=1.6';
}, false);
}
}
You can try it out on Huffduffer, Salter Cane, Principia Gastronomica and right here on Adactio.
Right now there’s still a little sluggishness between the initial pinch-zoom gesture and the scaling; the scale values (0.25 - 1.6) don’t seem to take effect immediately. A second pinch-zoom gesture is often required. If you have any ideas for improving the event capturing and propagation, dive in there.
Update: the bug has been fixed in iOS 6.