.

Tags:

Usually you don’t want to do this, but in a rare occasion, sometimes you want to be able to have a web page that is logically as wide as the physical pixels of the browser, especially on the mobile devices. In this situation, one CSS pixel (px) will be exactly one physical pixel as depicted on the screen.

The usual trick to fit the content into the viewport is by using the de-facto viewport meta tag, first popularized by iPhone and now widely support in many other mobile browsers. Those who design web sites optimized for mobile view are familiar with this technique. For example, the following will fix the width to the phone or tablet browser width and the user can’t scale (via pinching or menu buttons) at all:

<meta name=”viewport” content=”width=device-width initial-scale=1 maximum-scale=1 user-scalable=no”>

After Apple introduces higher-density screen, widely hyped as the retina display, the situation slightly changes. For compatibility with existing sites, the above trick still works. However, the content is simply scaled (in this case, twice). This means, a web page with the above viewport setting will still report 320 (px) as the screen.width (and window.innerWidth, not surprisingly) even on iPhone 4. Note that iPhone 4′s screen resolution is 640 × 960.

In order to detect the ratio between the device pixels and the logical pixels, there is window.devicePixelRatio. In the context of iOS, the value is 2 for a device using retina display, otherwise it is 1.

Now let’s consider Android. It does not come as a surprise that the above viewport meta is also supported. When Android-based phones with higher resolution started to be available in the market, compatibility with iPhone-targeted web sites via this trick needs to be retained. However, for phones with resolution like 480 × 800, it is not an integer multiple of 320 (for the width). In this case,  window.devicePixelRatio will have the value of 1.5.

Effectively, to reach 1:1 ratio of CSS pixel and physical pixel, we just need to compute the actual device width by multiplying  window.devicePixelRatio with  window.innerWidth and then adjust the viewport dynamically. The goal I’ve set for my experiment is however slightly more challenging: how can I do that without dynamic viewport modification unless it is absolutely necessary?

Fortunately for Android, we can do that rather easily, i.e. by customizing the scaling via another new setting: target-densityDpi. This was something specifically implemented for Android (see the corresponding commit).

Now let’s give it a try.

First of all, since Apple devices with retina display are arguably the most popular ones, let’s optimize for that use-case. We do that by setting the scale upfront to 0.5:

<meta name=”viewport” content=”width=device-width initial-scale=0.5 maximum-scale=0.5 user-scalable=no”>

For Android, we apply the density DPI approach and now it becomes:

<meta name=”viewport” content=”width=device-width target-densityDpi=device-dpi initial-scale=0.5 maximum-scale=0.5 user-scalable=no”>

Non-retina display is somehow still popular, e.g. iPad and previous generation of iPhone. To cater those users, we need to reset the scale back to 1 so that the viewport is not falsified (i.e. twice as large). We would do that by a simple JavaScript code (executed via window.onload):

if (window.devicePixelRatio === 1) {
    if (window.innerWidth === 2 * screen.width ||
        window.innerWidth === 2 * screen.height) {
        el = document.getElementById('viewport');
        el.setAttribute('content', 'width=device-width target-densityDpi=device-dpi ' +
            'initial-scale=1 maximum-scale=1 user-scalable=no');
        document.head.appendChild(el);
        width = window.innerWidth;
        height = window.innerHeight;
        if (width === 2 * screen.width) {
            width /= 2;
            height /= 2;
        }
    }
}

In the above, when checking the ratio between window.innerWidth and screen.width, apparently we need to check screen.height as well. This seems counterintuitive, however it is necessary because on iOS, screen.width and screen.height values are not swapped when the device switches orientation (landscape, portrait).

The Harmattan web browser in Nokia N9 (and N950) has this peculiar behavior where (after modifying the viewport)  window.innerWidth and screen.width are not properly updated. Hence, we need additional check for that and adjust the values (that division by 2) ourselves when necessary.

For an online test case, go to ariya.github.com/browser/viewport. If everything goes well, there should be two arrows (position:absolute) which point exactly to the left and right edge of the screen, respectively. It also shall report the screen width in physical pixels, something you can verify with the device specification. There is a perfectly square box, 100 × 100, which should be centered properly.

Using few devices I could test (iPhone, Nexus S, N950, iPad, Playbook, and TouchPad), the above strategy seems to work well. The proof lies in the following screenshots.

Note how this trick is still missing some tweaks. For example, switching orientation (portrait to landscape or vice versa) is not taken care of, it is left as an exercise for the brave reader. Even reloading the web page after orientation change does not always solve the issue (somehow the viewport settings are sticky in one way or another). Due to the complexity and different ways viewport is handling in different devices, bear in mind that this whole technique might not be 100% future-proof.

This is my endeavor so far and if you have a better alternative trick, please do share!

  • Kenneth Christiansen

    The N950 also supports the target-densitydpi feature of Android.

    Btw, why is it that you say the values are not updated. You are probably right about screen.width, but window.innerWidth should definitely change when the viewport size changes. I will have a look and file a bug if necessary.

    • Kenneth Christiansen

      I don’t have a device ready to test here, but I guess the issue with the N950 is that the viewport is updated asynchronously.

      • ariya

        What’s the best way to catch the new value?

  • Kenneth Christiansen

    The N950 also supports the target-densitydpi feature of Android.

    Btw, why is it that you say the values are not updated. You are probably right about screen.width, but window.innerWidth should definitely change when the viewport size changes. I will have a look and file a bug if necessary.

    • Kenneth Christiansen

      I don’t have a device ready to test here, but I guess the issue with the N950 is that the viewport is updated asynchronously.

      • ariya

        What’s the best way to catch the new value?

  • Kenneth Christiansen

    Have you tried leaving out ‘width=device-width’? or just using width=1 or similar. As the minimum-scale = maximum-scale = 0.5 will lock the scale and automatically expand the width to fill out the whole view. Also device-width is the same constant for landscape and portrait – it is relative to holding your device in portrait mode.

    Have you considered doing “minimum-scale = 1, maximum-scale = 1, target-densitydpi=device-dpi” and then if pixel ratio is different from 1.0 modify the 1 to 1 divided by the value?

    • ariya

      I haven’t tried leaving out width.

      As for 0.5 scaling by default, as stated above: I want to optimize for retina display first.

  • Kenneth Christiansen

    Have you tried leaving out ‘width=device-width’? or just using width=1 or similar. As the minimum-scale = maximum-scale = 0.5 will lock the scale and automatically expand the width to fill out the whole view. Also device-width is the same constant for landscape and portrait – it is relative to holding your device in portrait mode.

    Have you considered doing “minimum-scale = 1, maximum-scale = 1, target-densitydpi=device-dpi” and then if pixel ratio is different from 1.0 modify the 1 to 1 divided by the value?

    • ariya

      I haven’t tried leaving out width.

      As for 0.5 scaling by default, as stated above: I want to optimize for retina display first.

  • Kenneth Christiansen

    I tried doing it slightly different in a way that avoid first showing the page as 0.5 scale.

    This is tested on a non-retina iPhone and the N9.

    http://goo.gl/P9xXy

  • Kenneth Christiansen

    I tried doing it slightly different in a way that avoid first showing the page as 0.5 scale.

    This is tested on a non-retina iPhone and the N9.

    http://goo.gl/P9xXy

  • Petr Kapsia

    Thanks a lof for information, but please, typography .. I read this article with difficulties..

    • http://twitter.com/ariyahidayat Ariya Hidayat

      What kind of difficulties do you have? This site is just using the widely popular Twitter Bootstrap style.

  • Freddywang

    Bad news, latest Android Chrome 25.0.x, 26.0.x or above ignores target-densitydpi=device-dpi. It seems like density is set to 160dpi regardless of device physical ppi. CSS device pixel ratio can be any fraction number. That means, your 1px border can sometime be rendered as 2px or 1px or even missing pixel. E.g. Nexus 7, we get 1.332 device pixel ratio, 1.332*160 which yield 213ppi matching the device physical pixel density.

    Setting a standard of 160dpi is good, bad and it is ugly. The good, you get consistent interface dimension in physical unit (inch) across many different devices. You don’t get too small size button or flimsy thin navigation bar on too high ppi devices and you don’t get overly bloated size of interface on low ppi devices. Why 160dpi? It’s simply because most of the web interfaces are designed for iPad/iPhone web with 160dpi logical pixel density. Following such standard will gain immediate maximum compatibility with many web apps and web contents. The bad, 160dpi often is not any nice round multiply of original device ppi. This is where the ugly cat’s let out of the bag. You get those blurry edge, non-crisp square design, 2px replacing a single px line or border, missing 1px line and so on.

    On native Android app, they can resort to apply dip unit (density independent pixel unit). That helps to maintain solid crisp design. Unfortunately its mobile web counterpart doesn’t get the chance to leverage on that magic pixel. Gone those beautifully crafted pixel perfect designs. All thanks to Android fragmentation.

    • http://ariya.ofilabs.com/ Ariya Hidayat

      If Chrome has a bug, then it’s a bug (feel free to report that issue in their tracker). There’s no need to read too much into it and extrapolate further to “Android fragmentation” because the issue is on a different layer. People never cry out loud “PC fragmentation” if suddenly a particular version of MS Office has a peculiar bug.

      • Freddywang

        Not meant to emphasize on Android fragmentation. I was just worried that it is going to be some kind of implementation standard moving forward. Other webkit based browsers (“Browser” browser, Dolphin browser, etc) on Android face similar problem with dpi scaling as I mentioned in the scenario above. But only Chrome enforces a certain dpi (160dpi in current case). It completely ignores target-densitydpi attribute.

        This way will probably set a new trend from design perspective. Less pixel perfect design instead we all will have content focused design. Well, I start to see the rise of “Flat Design”. This could be one of the way to help improving mobile web experience facing a varying degree of devices with huge range of possible ppi. Content instead of tiny little pixel details of the design artifact.

        Back in the PC day, most people using PC didn’t give a shit about great design. I felt the same with today’s Android world.

      • Freddywang

        At this point, we wouldn’t know yet whether it’s a bug or it’s an intended feature.

  • Andrea Meleri

    Very interesting read! I have searched this information for some time and came up with solutions only for iOS. Do you know if there is an updated version of this article? Or is still valid? I cannot test my project on all mobiles…