Setting the Viewport to Scale to Fit Both Width and Height

52,299

Solution 1

It's possible to get a consistent behavior. But it's unfortunately very complex. I am working on a script that detects spoofed agents and dynamically rescale the viewport to desktop or other spoofed agents accordingly. I was also facing the zooming issue with Android/Chrome as well as the iOS emulator...

To get around it, you need to disable zooming and/or set the viewport twice. On the first pass, preferably inline in the <head> as you do now, you set your scale and disable user-scaling temporarily to prevent the zoom issue, using the same fixed value for all 3 scales like:

document.querySelector('meta[name=viewport]').setAttribute('content', 'width='+width+',minimum-scale='+scale+',maximum-scale='+scale+',initial-scale='+scale);

Then to restore zooming you set the viewport again on DOMContentLoaded, with the same scale, except that this time you set normal min/max scale values to restore user-scaling:

document.querySelector('meta[name=viewport]').setAttribute('content', 'width='+width+',minimum-scale=0,maximum-scale=10');

In your context, because the layout is fixed and larger than the viewport, initial-scale='+scale is perhaps needed for a more sound alternative for DOMContentLoaded:

document.querySelector('meta[name=viewport]').setAttribute('content', 'width='+width+',minimum-scale=0,maximum-scale=10,initial-scale='+scale);

That should get the viewport to rescale as you would like in Webkit browsers without zooming problems. I say only in webkit because sadly IE and Firefox do not support changing the viewport as per this Browser Compatibility Table for Viewports, Dimensions and Device Pixel Ratios shows: http://www.quirksmode.org/mobile/tableViewport.html

IE has its own way to change the viewport dynamically which is actually needed for IE snap modes to be responsive. http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/

So for IEMobile and IE SnapMode (IE10&11) you need to dynamically insert an inline <style> in the <head> with something like.

<script>
 var s = document.createElement('style');
 s.appendChild(document.createTextNode('@-ms-viewport{width:'+width+'px')+';}'));
 document.head.appendChild(s);
</script>

And unfortunately, Firefox has neither: The viewport is set for once and for all as the above compatibility table shows. At the moment, for lack of other methods, using CSS Transform (as @imcg pointed out) is the only way to alter the viewport in FireFox Mobile on Android or Gecko OS. I have just tested it and it works in the context of a fixed size design. (In "Responsive Design context", the layout can be rescaled larger via CSS Transform, say at desktop size on a phone, but Firefox still read the phone size MQs. So that's something to be mindful off in RWD context. /aside from webkit)

Though, I have noticed some random Webkit crashes with CSSTransform on Android so I would recommend the viewport method for Safari/Chrome/Opera as more reliable one.

In addition, in order to get cross-browser reliability for the viewport width, you also have to face/fix the overall inconsistency between browsers for innerWidth (note that documentElement.clientWidth is much more reliable to get the accurate layout pixel width over innerWidth) and you also have to deal with devicePixelRatio discrepancies as indicated on the quirksmode.org table.

Update: Actually after some more thought into a solution for my own problem with Firefox, I just found out a way to override the viewport in Firefox Mobile, using document.write(), applied just once:

document.write('<meta name="viewport" content="width='+width+'px,initial-scale='+scale+'">');

Just tested this successfully in both Webkit and Firefox with no zooming issues. I can't test on Window Phones, so I am not sure itf document.write works for IEMobile...

Solution 2

I know this is two years late, but I spent a lot of time working on this problem, and would like to share my solution. I found the original question to be very helpful, so I feel that posting this answer is my way of giving back. My solution works on an iPhone6 and a 7" Galaxy Tab. I don't know how it fares on other devices, but I'm guessing it should mostly behave.

I separated the viewport logic into an external file so that it is easy to reuse. First, here's how you would use the code:

<HTML>
<HEAD>    
  <SCRIPT type="text/javascript" src="AutoViewport.js"></SCRIPT>
</HEAD>
<BODY>
  <SCRIPT>
  AutoViewport.setDimensions(yourNeededWidth, yourNeededHeight);
  </SCRIPT>
  <!-- The rest of your HTML goes here -->
</BODY>
</HTML>

In actuality, I padded my desired width and height by a slight amount (15 pixels or so) so that my intended display was framed nicely. Also please note from the usage code that you do not have to specify a viewport tag in your HTML. My library will automatically create one for you if one does not already exist. Here is AutoViewport.js:

/** Steven Yang, July 2016
Based on http://stackoverflow.com/questions/21419404/setting-the-viewport-to-scale-to-fit-both-width-and-height , this Javascript code allows you to 
cause the viewport to auto-adjust based on a desired pixel width and height
that must be visible on the screen.

This code has been tested on an iPhone6 and a 7" Samsung Galaxy Tab.
In my case, I have a game with the exact dimensions of 990 x 660.  This
script allows me to make the game render within the screen, regardless 
of whether you are in landscape or portrait mode, and it works even
when you hit refresh or rotate your device.

Please use this code freely.  Credit is appreciated, but not required!
*/

function AutoViewport() {}

AutoViewport.setDimensions = function(requiredWidth, requiredHeight) {

/* Conditionally adds a default viewport tag if it does not already exist. */
var insertViewport = function () {

  // do not create if viewport tag already exists
  if (document.querySelector('meta[name="viewport"]'))
    return;

  var viewPortTag=document.createElement('meta');
  viewPortTag.id="viewport";
  viewPortTag.name = "viewport";
  viewPortTag.content = "width=max-device-width, height=max-device-height,initial-scale=1.0";
  document.getElementsByTagName('head')[0].appendChild(viewPortTag);
};

var isPortraitOrientation = function() {
  switch(window.orientation) {  
  case -90:
  case 90:
  return false;
  }

  return true;
 };

var getDisplayWidth = function() {
  if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
    if (isPortraitOrientation())
      return screen.width;
    else
      return screen.height;
  }

  return screen.width;
}

var getDisplayHeight = function() {
  if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
    if (isPortraitOrientation())
      return screen.height;
    else
      return screen.width;
  }

  // I subtract 180 here to compensate for the address bar.  This is imperfect, but seems to work for my Android tablet using Chrome.
  return screen.height - 180;
}

var adjustViewport = function(requiredWidth, requiredHeight) {

  if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)){

    var actual_height = getDisplayHeight();
    var actual_width = getDisplayWidth();

    var min_width = requiredWidth;
    var min_height = requiredHeight;

    var ratio = Math.min(actual_width / min_width, actual_height / min_height);

    document.querySelector('meta[name="viewport"]').setAttribute('content', 'initial-scale=' + ratio + ', maximum-scale=' + ratio + ', minimum-scale=' + ratio + ', user-scalable=yes, width=' + actual_width);
}    
};

  insertViewport();
  adjustViewport(requiredWidth, requiredHeight);
  window.addEventListener('orientationchange', function() {
    adjustViewport(requiredWidth, requiredHeight);      
  });
};

If you compare my code closely with the original code found in the question, you will notice a few differences. For example, I never rely on the viewport width or height. Instead, I rely on the screen object. This is important because as you refresh your page or rotate your screen, the viewport width and height can change, but screen.width and screen.height never change. The next thing you will notice is that I don't do the check for (ratio<1). When refreshing or rotating the screen, that check was causing inconsistency, so I removed it. Also, I included a handler for screen rotation.

Finally, I'd just like to say thank you to the person who created this question for laying the groundwork, which saved me time!

Solution 3

If you can't get consistent behavior across devices by changing the viewport meta tag, it's possible to zoom without changing the dimensions using CSS3 transforms:

if (ratio < 1) {
    var box = document.getElementsByTagName('div')[0];
    box.style.webkitTransform = 'scale('+ratio+')';
    box.style.webkitTransformOrigin = '0 0';
}

console.log(box.offsetWidth); // always original width
console.log(box.getBoundingClientRect().width); // new width with scaling applied

Note I've omitted any vendor prefixes other than webkit here in order to keep it simple.

To center the scaled div you could use the translate tranforms:

var x = (actual_width - min_width * ratio) / 2;
var y = (actual_height - min_height * ratio) / 2;
box.style.webkitTransform = 'translateX('+x+'px) translateY('+y+'px) scale('+ratio+')';
box.style.webkitTransformOrigin = '0 0';

Solution 4

Replace your viewport with this :

<META NAME="viewport" CONTENT="width=device-width, height=device-height, initial-scale=1, user-scalable=no"/>

The user-scalable=0 here shall do the job for you.

This shall work for you.If it still doesn't work for we will have to extend the viewport so replace your viewport and add this:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, target-densitydpi=medium-dpi, user-scalable=0" />

For your javascript error have a look at this link:

scale fit mobile web content using viewport meta tag

Share:
52,299
Nick
Author by

Nick

Updated on July 29, 2022

Comments

  • Nick
    Nick almost 2 years

    I'm working on a website that fits within a specific width and height (an 885x610 div with a 1px border and 3px top margin). I would like the user to never have to scroll or zoom in order to see the entire div; it should always be fully visible. Since devices have a wide variety of resolutions and aspect ratios, the idea that came to mind was to set the "viewport" meta tag dynamically with JavaScript. This way, the div will always be the same dimensions, different devices will have to be zoomed differently in order to fit the entire div in their viewport. I tried out my idea and got some strange results.

    The following code works on the first page load (tested in Chrome 32.0.1700.99 on Android 4.4.0), but as I refresh, the zoom level changes around. Also, if I comment out the alert, it doesn't work even on the first page load.

    Fiddle

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0">
            <script type="text/javascript">
                function getViewportWidth() {
                    if (window.innerWidth) {
                        return window.innerWidth;
                    }
                    else if (document.body && document.body.offsetWidth) {
                        return document.body.offsetWidth;
                    }
                    else {
                        return 0;
                    }
                }
    
                function getViewportHeight() {
                    if (window.innerHeight) {
                        return window.innerHeight;
                    }
                    else if (document.body && document.body.offsetHeight) {
                        return document.body.offsetHeight;
                    }
                    else {
                        return 0;
                    }
                }
    
                if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) {
                    var actual_width = getViewportWidth();
                    var actual_height = getViewportHeight();
    
                    var min_width = 887;
                    var min_height = 615;
    
                    var ratio = Math.min(actual_width / min_width, actual_height / min_height);
    
                    if (ratio < 1) {
                        document.querySelector('meta[name="viewport"]').setAttribute('content', 'initial-scale=' + ratio + ', maximum-scale=' + ratio + ', minimum-scale=' + ratio + ', user-scalable=yes, width=' + actual_width);
                    }
                }
    
                alert(document.querySelector('meta[name="viewport"]').getAttribute('content'));
            </script>
            <title>Test</title>
            <style>
                body {
                    margin: 0;
                }
    
                div {
                    margin: 3px auto 0;
                    width: 885px;
                    height: 610px;
                    border: 1px solid #f00;
                    background-color: #fdd;
                }
            </style>
        </head>
        <body>
            <div>
                This div is 885x610 (ratio is in between 4:3 and 16:10) with a 1px border and 3px top margin, making a total of 887x615.
            </div>
        </body>
    </html>
    

    What can I do to have this website scale to fit both the width and the height?

  • Karan Thakkar
    Karan Thakkar over 10 years
    you'll definitely get a horizontal scroll-bar :(
  • Nick
    Nick over 10 years
    There is, indeed, a horizontal scrollbar. Also, I don't think I was clear enough as to what I was after. The div should always be the same dimensions (885x610 plus a 1px border and 3px top margin). It's just that it should be zoomed differently on different devices in order to get it to fit in the viewport. I apologize for not being clear enough and have updated my question to be more clear for others. Thank you very much for your time!
  • Scott Bartell
    Scott Bartell over 10 years
    Why do you want the div to always be the same dimensions?
  • Nick
    Nick over 10 years
    @ScottBartell It's just the way this particular website was designed (I did not design it). Throughout the website, the div's background image changes and other things like that and everything depends on the div being these exact dimensions. Yeah, probably not the greatest design, but that's the situation I'm in. I've already tested it on mobile devices and even on phones the website is still usable even when zoomed way out. It's just that I need it to be scaled that way automatically on every device.
  • Nick
    Nick over 10 years
    This seems to mostly work. Thank you very much! Do you know how I can keep the div centered? Also, I think in my case where the div is the only thing in the body, it's better to apply the transform to the body rather than the div. Otherwise, there is a lot of white space under the div (presumably taking up the same amount of space the div would take up if it didn't have a transform applied). At any rate, this is much closer to what I want than what I've been able to come up with, so thank you!
  • imcg
    imcg over 10 years
    Sure, sticking with transforms, you can use translate to center it. Will add an e.g to the answer.
  • Nick
    Nick over 10 years
    That worked. Thanks! However, various things throughout the site aren't working now, namely the things that are absolutely positioned based on the offset of elements that are within the div, which is now scaled. And by the way, I had to switch back to the div being scaled instead of the body because Twitter Bootstrap's modal backdrop wasn't covering the whole page. So now I have white space under the div again. I'm not sure this approach will work after all. But since you've put so much effort into this, I will still award the bounty if nobody else provides an answer that works for me.
  • user3274745
    user3274745 over 10 years
    If your answer is still not solved please let me know why the problem still persists and i would get onto it
  • hexalys
    hexalys over 10 years
    Those viewport rules are not rescaling anything. They are just leaving it at the device width a 1.0 scale... The question is about rescaling the viewport from the default device-width(s) to a custom scale dynamically, in relation with device conditions and browser viewport dimensions.
  • user3274745
    user3274745 over 10 years
    @hexalys if you try the above options the above options should work
  • user3274745
    user3274745 over 10 years
    I have edited the answer @hexalys to suit his javascript
  • hexalys
    hexalys over 10 years
    Again, you are misunderstanding the question. The site/page is a fixed width. No need for media queries here. It's a lot more complicated. Don't dig a bigger hole for yourself. :)
  • Nick
    Nick over 10 years
    @user3274745 hexalys' comments are correct. You (and the people who upvoted this answer) appear to have misunderstood the question. I've had a hard time wording the question in a way that's easily understood, so I apologize for that. Thanks for your time, though! And yes, I did test your two solutions, just in case. They didn't work.
  • Nick
    Nick over 10 years
    I got somewhat close by changing your updateScale function to: containerHeight=615; containerWidth=887; scaleW=window.outerWidth/containerWidth; scaleH=window.outerHeight/containerHeight; ratio=Math.min(scaleW,scaleH); $('head').append('<meta name="viewport" content="width='+containerWidth+'; initial-scale='+ratio+'; maximum-scale=1.0; user-scalable=0;">');. It seems to work in Chrome for Android, but not Android's stock browser and I don't think it works on iOS either. But thanks for your time!
  • Nick
    Nick over 10 years
    +1 for your efforts and all the useful information! There's definitely no perfect solution to this. I tried yours and, when restoring zooming, I had better results using 'initial-scale=' + ratio instead of minimum-scale=1,maximum-scale=10. However, your solution doesn't seem to work in Android's stock browser or, as you mentioned, Firefox for Android. @imcg's solution works in Android's stock browser and it also works in Firefox for Android (when you replace all instances of webkit with Moz), so even though that solution didn't work in my case, it will probably work for others.
  • hexalys
    hexalys over 10 years
    My bad, min-scale is actually 0 to restore a proper zoom, I'll correct that typo. But you indeed may have different results, since your size is bigger that the viewport, and fixed. The basis for my methods is a Responsive Design to adapt to a larger size (i.e. switch it to desktop width). So the context is different and it may have different caveats. Forcing initial-scale='+scale on the second pass may indeed help in your case. That wasn't needed for me. Also note you that you need a small delay between the 1st and 2nd settings of the viewport or that won't work. A few ms is usually enough.
  • Dan1121
    Dan1121 over 10 years
    I tested it on iphone (ios7) and works fine. not sure with stock Android though.
  • Nick
    Nick over 10 years
    Thank you very much for all the time and effort you've put into this. Unfortunately, the fact that it doesn't work in Android's stock browser is a deal breaker in my situation. I'm sure other people can use this information, though. You've been very helpful!
  • hexalys
    hexalys over 10 years
    @Nick No worries been working on this script for a month now. :) Which stock browser are you testing on? I am currently testing the stock browser on Android 4.3 SDK Emulator, it works fine with the document.write() method.
  • Nick
    Nick over 10 years
    I'm using Android 4.2.2
  • hexalys
    hexalys over 10 years
    @nick Here is a working test case for you: hexanet.com/test/viewport-rescale.html Can't do a JSFiddle because it doesn't like document.write()... I only address/adapt the width but you could surely adapt from there to handle the height. Also I realized the querySelector in my answer had typos I'll fix that.
  • user3274745
    user3274745 over 10 years
    @Nick have a look at the link i just attached
  • Nick
    Nick over 10 years
    The link you provided does seem to work in Firefox for Android (after you scroll down to make the address bar and other interface elements go out of view, that is), so that's good. I still couldn't get it to work with Android 4.2.2's stock browser, though. Thanks, again, for all your efforts!
  • hexalys
    hexalys over 10 years
    Ok, well all my sdk/x86 test environments work. I'll have to try and find real devices to do additional testing. As I indicated, it's a very complex exercise with the differences between browsers.
  • Pegasus Epsilon
    Pegasus Epsilon about 9 years
    @user3274745 Adding links won't help the fact that your answer doesn't do what is needed. Also, please don't use uppercase HTML tags. They aren't universally parsed properly. (<TAG> no, <tag> yes)
  • Magnus
    Magnus about 6 years
    Thanks, works very nicely. I only had the need to set the width, so without analysing your code :) I found that I could just set the height to 0.
  • Magnus
    Magnus about 6 years
    On a Samsung S7 I got some problems if I alternated from landscape to portrait and back to landscape. The landscape viewport was then not good. I managed to solve it by changing the eventlistener to setTimeout(function () { adjustViewport(requiredWidth, requiredHeight); }, 200); (50 ms was on the border of being too short, 200 ms seems a good choice).
  • Steven
    Steven about 6 years
    Interesting solution, Magnus! I still use AutoViewport with my own code, so I will consider integrating your change.