Proportional scaling for responsive websites with REMux

Introduction

Most likely all readers of this article will agree that responsive web design has been one of the biggest subjects in 2012. With the ongoing success of mobile device sales this trend will continue, for sure. As a senior front- and backend developer with a strong interest in research and development in my company I am responsible to evaluate techniques like RWD as well as others. Whenever I received a link to a totally new CSS grid system I became more and more skeptical. For some reason these things did not feel "right", at least not for me, without really knowing any reason why.

I than happened to come across a great article by Ian Yates titled "Life Beyond 960px: Designing for Large Screens"1 which introduced me to the term "Screen real estate" in the sense of RWD. Prior to that I did some deeper research using REM units in CSS which was a fortunate coincidence. Suddenly I knew what felt wrong:

When talking about RWD in general we are talking about devices below the target width of our layouts most of the time. But what about normal Screens with full HD resolution? Most of you will agree that a (non RWD) website with a target width of 960px looks a bit odd or lost on such a screen. Things are becoming more obvious when we talk about people accessing our websites with a 60" Plasma TV. Sure, these TV sets will most likely still only have full HD resolution. But keep in mind that whoever sits in front of them is probably at least 4m/10f away from the screen.

Current situation

Whether we do mobile, tablet or desktop first - most of us will end up having at least 3 media query breakpoints and an equal amount of more or less different layouts. Or we will use a grid system which will automagically change the composition of our sites' content elements. Or a combination of both. Both approaches have their drawbacks if we are to support more and more different resolutions and different viewing situations:

  • more breakpoints = more layouts = more work
  • hard to balance flexibility and proportions of elements
  • jerky looking re-stacking of elements
  • limited by the amount of content to fill the viewport
  • teach your art that a grid is more than some guides in photoshop

And whatever way we choose to be most appropriate:

  • the actual content (including every single element) will not scale proportionally

Proof of concept

What came to my mind was the idea of a solely REM-driven stylesheet based on the body font-size of the layout and its total width. If this idea worked I could easily scale all content by simply changing the font-size of the HTML element. This concept would solve the biggest challenge:

  • layouts scale almost perfectly within their boundaries

Keep in mind, though, that REM is only supported in IE9+ and all other current browsers. For older browsers a px based fallback is possible, see the example mixins below.

I started to do some proof-of-concept work with this site (which I never had the time to start working on anyway). The static version worked out really well after I had developed some LESS mixins that convert pixel units (taken directly from the layout) to REM units based on the 14px font-size I decided to use (see2 for a demo).

LESS mixins

The mixins basically are of two types, one for single parameters properties like "font-size" and another one for properties with multiple parameters like "border". Both rely on two LESS variables that must be defined.

@remuxFontsize: 14px;  // base font size
@remuxFallback: false; // switch to include px fallback

.remux(font-size, @value) {
    @processingRem: ((round((@value / @fontsize-base) * 10000000)) / 10000000);

    .result(@value, @fallback) when (@fallback = true) {
        font-size: ~"@{value}px";
        font-size: ~"@{processingRem}rem";
    }
    .result(@value, @fallback) when (@fallback = false) {
        font-size: ~"@{processingRem}rem";
    }

    .result(@value, @remuxFallback);
}

.remux(border, @value) {
    @processingRem: ~`(function() { var base = parseFloat("@{remuxFontsize}"); return "@{value}".replace(/[\[\]]/g, '').replace(/([0-9]*[.]{0,1}[0-9]*px)/gi, function() { return ((Math.round((parseFloat(arguments[0]) / base) * 10000000)) / 10000000) + 'rem'; }); }())`;

    .result(true) {
        border: @value;
        border: @processingRem;
    }
    .result(false) {
        border: @processingRem;
    }

    .result(@remuxFallback);
}

After having finished the mixins usage is quite simple:

font-size: 16px;                   // instead of writing
.remux(font-size, 16px);           // you now use

border: 7px solid #000;            // instead of writing
.remux(border, ~"7px solid #000"); // you now use

In fact, at the point I switched my site's CSS to use REM I was already able to change the font-size of the HTML-element via developer tools and see the site scale almost perfectly!

What I needed next was a solution to calculate this dynamically via JavaScript on "resize" and "orientationchange" events. The basic calculation is really simple:

;(function(window, document, undefined) {
    'use strict';

    var targetLayoutWidth = 980,
        baseFontsize      = 14,
        htmlElement       = document.getElementsByTagName('html')[0],
        documentElement   = document.documentElement;

    function updateFontsize() {
        var currentFontsize = baseFontsize * (documentElement.offsetWidth / targetLayoutWidth);

        htmlElement.style.fontSize = currentFontsize + 'px';
    }

    window.addEventListener('resize', updateFontsize, false);
    window.addEventListener('orientationchange', updateFontsize, false);

    updateFontsize();
}(window, document));

But when putting the result into the HTML-element I realized that floating point numbers cause problems when rounded by the browser for rendering (see3 for a demo). So I ended up having to floor the font-size which eliminated any rounding glitches but made the scaling less "fluid" - feels still more than sufficient for me though (see4 for a demo).

Next on the list was some extended testing on my iPad2 with the HTML viewport set to "device-width". I admit having been a bit surprised when not only everything worked after the initial page load but also pinch-to-zoom and changes in device orientation behaved as desired.

Implementing layouts

Now that I was able to infinitely scale my layout I started implementing breakpoints. Breakpoints will still be required because infinite scaling (down to zero or positive infinity) of the font-size does not really make sense although technically possible.

I started planning the structure of my future layout object by determining what was required which lead to the following JavaScript object structure:

var layouts = {
    'ID': {
        width:      980,            // designs target width
        base:       14,             // designs base font-size
        min:        10,             // minimum font-size
        max:        18,             // maximum font-size
        breakpoint: 980 * (10 / 14) // minimum breakpoint
    }
}

Now that layouts could be defined I had to add them to the update function which had some more impact than I had thought (see5 for a demo):

;(function(window, document, undefined) {
    'use strict';

    var htmlElement     = document.getElementsByTagName('html')[0],
        documentElement = document.documentElement,
        layouts = {
            mobile: {
                width:      420,
                base:       14,
                min:        10,
                max:        23,
                breakpoint: 420 * (10 / 14)
            },
            desktop: {
                width:      980,
                base:       14,
                min:        10,
                max:        18,
                breakpoint: 980 * (10 / 14)
            }
        },
        state = {
            size:   null,
            layout: null
        };

    function updateFontsize() {
        var width, id, layout, current, size;

        width = documentElement.offsetWidth;

        for(id in layouts) {
            if(layouts[id].breakpoint && width >= layouts[id].breakpoint) {
                layout = id;
            }
        }

        if(layout !== state.layout) {
            state.layout = layout;

            htmlElement.setAttribute('data-layout', layout);
        }

        current = layouts[state.layout];

        size = Math.max(current.min, Math.min(current.max, Math.floor(current.base * (width / current.width))));

        if(size !== state.size) {
            state.size = size;

            htmlElement.style.fontSize = size + 'px';
        }
    }

    window.addEventListener('resize', updateFontsize, false);
    window.addEventListener('orientationchange', updateFontsize, false);

    updateFontsize();
}(window, document));

So now I had a first prototype that I tested on Chrome, Safari, FF17+ and IE9 where REMux should work. I had some bugs in the CSS but REMux itself worked pretty well beside some minor bugs that were easily fixed.

See it in action

I already mentioned that REMux is heavily (ab)used on this site. Beside that there are some pens up on CodePen which were already linked throughout the article. In addition there is another pen showing what I ended up with6.

I hope to get some documentation ready soon - but the code is not that much nor hard to read…

What's next?

In fact REMux works so well that I decided to make it part of my growing JavaScript library which can be found on GitHub. Within the last couple of weeks I also added some more features that are missing in this basic article:

  • AMD compatible (require.js)
  • addLayout() method to add layouts without altering the script
  • ratios for pixel density, zoom level7, font-size, total and images (the latter excluding zoom level)
  • getState() method that will return all sizes and ratios
  • emits events "ratiochange" and "layoutchange" with the current state passed as argument
  • "flood protection" mechanism for repeated input events8 for better performance
  • extendable by an object extension mechanism

The standalone version of REMux can be found in the "packages" folder of my GitHub repository and weighs approx. 4kb minified and gzipped. It does not depend on jQuery or any other bigger library. Its dependencies to other components of my library are all included in the package. All components of my JavaScript library are dual licensed under the MIT and GPL license.

Feel free to download and use it in your projects but remember to give feedback and report bugs so I can make it even better!

This article was originally published on CSS-Tricks on february 10, 2013. Therefore very special thanks go to Chris Coyier for giving me the opportunity and the honor to get something published on one of my favorite sites!