This wide- and large- screen layout may not work quite right without Javascript.

Maybe enable Javascript, then try again.

Chuck Kollars` Personal Home Fiddling with Home PCs

Alternative to Media Queries

Media Query Use

Nowadays it's almost required to have the layout of a web page change to accommodate different devices. And it may be nice too to make the the web page layout adjust instantly to device changes (such as rotating from portrait orientation to landscape orientation). Changing on the fly like this requires quick changeovers to alternate chunks of CSS. Techniques for making web pages adapt to different devices coalesce under various rubrics: liquid layout or fluid layout or adaptive design or responsive design. When pointing different environments to different chunks of CSS, likely all that's needed is the CSS feature Media Queries.

Several different detailed strategies for handling varying devices are possible. Some strategies are mainly concerned with the size of the physical screen, while others are mainly concerned with the size of the logical viewport. Some strategies tightly restrict (or even try to eliminate) the viewport, while others control it only loosely (or even leave it entirely unconstrained). Regardless, all such strategies are likely to be implemented using Media Queries.

In theory, Media Queries can handle everything. The conventional wisdom seems to be that Media Queries are the best thing since sliced bread.

Problems With Media Queries

But when I started to use Media Queries extensively in real life, I discovered that as a practical matter they bring along quite a few issues. Media Queries can work quite well in environments -such as some corporate intranets- where the audience is either restricted to only the latest standards and browsers or is uniform (or both). But when websites are visited by the general public using a very wide range of browsers that should all be well supported, Media Queries become problematic.

Problems with Media Queries include:

(This was written in Fall 2012. Technology marches on. Some of the problems listed above are probably less significant these days.)

An Alternative Approach

I find trying to deal with all these issues with only the non-procedural specifications of HTML/CSS awfully frustrating. I'm confident that if I could just write a program, I could skirt the majority of these problems. If only I could move my CSS decisions into some procedural language (i.e. any computer language with at least a conditional statement and a looping construct).

But that sounds like a tool that's already in use on most websites: Javascript! Instead of different <link statements each being enabled or disabled by Media Queries, Javascript could create just the one <link statement that was exactly right for each situation, then insert it into the webpage using ordinary DOM manipulation techniques.

This alternate approach of using Javascript rather than Media Queries is more than just simple to implement. It's very flexible, and it can handle complex logic that would be quite difficult to express as Media Queries.

Several Media Query equivalent shims written in Javascript to support browsers that don't have the needed Media Queries are readily available. But my experience is these are sufficiently limited and obscure that in the long run support is broader and maintenance is easier if you just create your own Javascript tailored to your own environment.

An obvious problem with this alternative approach is visiting browsers that don't support Javascript or that have Javascript turned off. Given the relative ubiquity of Javascript support in browsers, the widespread use of AJAX, and the ready availability of security precautions more sophisiticated than just completely disabling Javascript for all sites, it's probably safe to assume that almost all visitors provide Javascript. Still, it's prudent to arrange your CSS in such a way that if the code doesn't run at all and the visitor gets only the base CSS, the website will still be readable and usable. This arrangement of CSS is somewhat more important if you're using Javascript than if you're using plain old Media Queries, but you will have already arranged your CSS this way anyway if support for smaller older (i.e. slow and dumb) handhelds is important to you.

A Detailed Specific Example

Here's a specific example. Please note that:

The general approach of using Javascript rather than Media Queries to get the right CSS to each browser will always remain, even though the specific code for other environments may appear quite different.

This particular example is concerned exclusively with the size of the physical screen (not the logical or virtual or viewport size). This particular example assumes the viewport feature has already been controlled (probably by particular parameters on a meta...viewport... statement). However the general approach (depending on Javascript rather than Media Queries) does not depend on this assumption; it applies equally to other strategies, such as those mainly concerned with the unconstrained viewport size.

This example code neither requires nor interferes with the possible use of cookies.

The specific densities, dimensions and aspect ratios hard coded in this example code are all magic numbers, determined by methods not discussed here. Supposedly they do a good job of distinguishing different categories of device. You may or may not wish to use these same specific numbers.

Several tricks for supporting the widest possible range of browers are embedded in this example code but not discussed in any detail. Also, this code may have been optimized to the point of not being immediately fully comprehensible. So depending on what your goal is, you may wish to analyze this example code rather closely. Of course, it's assumed that for actual production use, similar code will be minified and compressed.

Although I have not actually tested either the defer or async attributes of the <script tag with this example code, I expect they would not work reliably with some browsers.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>...</title>
<meta name="viewport" content="width=device-width">
<!--   ...   -->
<link href="common.css" rel="stylesheet" type="text/css">
<script type="text/javascript">
	/* our ultimate goal here is to determine the physical size of the device, 
	    so we can then specify different layouts for the different sizes 
	    HOWEVER doing this is somewhat tricky, because:
	    a) even after the logical "viewport" has been controlled,
	       two devices of the same physical size can have wildly different
	      "px" dimensions because of different densities
	    b) the CSS "inch" metric is _not_ useful for this purpose, as the
	      "CSS inch" is _defined_ as 96 pixels, and so may be very different
	      from what one would measure with a ruler
	   so to determine size with any accuracy, we first need to determine density,
	    either as "dots per inch" (a physical metric, 
	    closer to what a ruler would report than "pixels per inch"), 
	    or as the density ratio of "dots per inch" to "pixels per inch" 
	   then we can go on to determine the size
	   and finally specify loading of the one needed CSS file for that size
	*/


	function formatvalue(numerator, denominator, propertyname) {
		/* this routine formats a value either as a real (decimal) number with 
		    finite precision, or as a ratio (fraction) of integers,
		    as appropriate to the query
		   its parameters must be in terms of a ratio of integers, 
		    because converting ratio->real whenever necessary is simple, 
		    but converting real->ratio whenever necessary would be more difficult
		*/
		/* (this function works quickly and lightly by assuming that the 
		    "propertyname" argument is all lower case - if "propertyname" 
		    contains upper case characters, it may not work right)
		*/
		/* both numerator and denominator assumed always non-zero,
		    else another line of code would be needed 
		    to return correct results no matter what */
		if (propertyname.indexOf('ratio') >= 0) return(numerator + '/' + denominator);
		else return((numerator/denominator).toFixed(2));
	}


	var i, j = 0, k, l = 0;
	/* indices for loops etc., 
	    in the end these encode the intermediate and final results
	   note we _assume_ these are never out of range nor negative when read,
	    which will be true in the code that follows, but may not be true 
	    and so cause bad behavior if portions of this code are transplanted 
	    without sufficient care
	   i == index which density resolution test works, reversed (0 => no usable query supported):
	    resolution(dppx), resolution(dpi), -webkit-device-resolution,
	    -moz-device-resolution, -o-device-resolution, none 
	   j == resolution as determined, reversed (0 => could not determine density):
	    2+/xhi, 1.5/hi, 1/med, 0.75/lo, ?/no
	   k == breakpoints:
	    minimum px for unknown size, minimum px for size tablet, 
	    minimum px for size desktop, minimum px for size widescreen,
	    minimum px for size widescreen if aspect ratio is clearly elongated
	   l == determined size category (0 => no additional CSS file needed):
	    handheld/default, tablet, desktop, widescreen
	*/

	/* try to get devicePixelRatio either by calculating it from Javascript vars (if possible),
	    or taking it directly from a Javascript variable (if available),
	    or else from the matchMedia(...) function (if available)
	    (the relatively new matchMedia(...) function provides easy access
	    to arbitrary Media Queries from within Javascript) */ 
	if (screen.deviceXDPI) {
		var dPI = screen.deviceXDPI;
		/* dPI is just a shortcut temporary variable with a very short name */
		j=1;
		/* just the existence of any non-zero screen.deviceXDPI variable
		    sets the default density to "lo-res" (rather than "no-res") */
		if (dPI > 120) j = 2;
		if (dPI > 200) j = 3;
		if (dPI > 280) j = 4;
	} else if (window.devicePixelRatio) { /* density-from-Javascript variable exists and is not zero */
		/* ("screen.devicePixelRatio" seems more sensible than "window.devicePixelRatio"
		     ...but we have to play the hand we're dealt */
		/* "i" is not set on this path, as we don't need it to determine "j" this way */
		var dPR = window.devicePixelRatio;
		/* dPR is just a temporary variable to avoid retyping a long name several times*/
		j = 1; 	
		/* just the existence of any non-zero window.devicePixelRatio variable
		    sets the default density to at least "lo-res" (rather than "no-res") */
		if (dPR >= 1) j = 2;
		if (dPR >= 1.5) j = 3;
		if (dPR >= 2) j = 4;
		/* note order of statememts above is intentional/necessary, 
		    as higher density resolutions overwrite lower
		    so the result is the highest density class that matches */
		/* also note all values >2 (such as 2.25) will be treated as 2 */
	} else if (typeof window.matchMedia === 'function') {
		/* find out which query is supported by asking silly questions
		    that we know always have positive answers, so we can confidently
		    interpret a negative answer as "query not supported"
		   exit loop on first hit 
		    (more important/common queries are tested first) */
		var properties = [ '', '-o-min-device-pixel-ratio', 'min--moz-device-pixel-ratio',
		 '-webkit-min-device-pixel-ratio', 'min-resolution', 'min-resolution' ];
		var scales = [ 0, 1, 1, 1, 160, 1 ];
		/* variable "scales" pre-converts queries from "dpi" to "dppx" */
		var units = [ '', '', '', '', 'dpi', 'dppx' ];
		/* the above three arrays together (they make no sense separately)
		    define possible media queries we might use 
		    (just one identical index is used into all three) */
		for (i=5; i>0; --i) {
			if (window.matchMedia('only all and (' + properties[i] + ': ' +
			 formatvalue(scales[i], 10, properties[i]) + units[i] + ')').matches) break;
		}
		/* if no hit, i will be 0 when loop exited */
		if (i) {
			/* find out device density resolution by executing several queries,
			    combining the supported property name we found above with different values */
			var numerators = [ 0, 3, 1, 3, 2 ];
			var denominators = [ 1, 4, 1, 2, 1 ];
			/* the above two arrays together (they make no sense 
			    separately) define the conventional device density 
			    resolutions (0, 0.75, 1, 1.5, 2) as fractions */
			for (j=4; j>0; --j) {
				if (window.matchMedia('only all and (' + properties[i] + ': ' +
				 formatvalue(scales[i]*numerators[j], denominators[j], properties[i]) + units[i] + ')').matches) break;
			}
			/* if no hit, j will be 0 when loop exited */
		}
		/* the case of (!i) [i===0] is already handled by having initialized j = 0 
		    so the default value is already right in this case */
	}

	/* density -> breakpoints */
	k = [ [493, 987, 1481, 1281], [296, 592, 888, 769], [493, 987, 1481, 1281],
	 [740, 1481, 2221, 1921], [987, 1974, 2962, 2562] ][j];
	/* the above breakpoints are specified by the user to provide the 
	    desired behavioral results ...they are not calculated here, 
	    and they don't make any sense in any "abstract" way */
	var scrwid = screen.width;
	var scrhit = screen.height;
	if (window.outerWidth && window.outerHeight && (window.outerWidth <= (scrwid/2)) && (window.outerHeight <= (scrhit/2))) {
		/* apparently "meta...viewport..." exists but isn't applied yet
		    and so the screen dimensions are incorrect,
		    but correct screen dimensions are available elsewhere anyway */
		scrwid = window.outerWidth;
		scrhit = window.outerHeight;
	}
	var scrasp = (scrwid/scrhit).toFixed(2);
	/* use our specified breakpoints to figure out the index of the size category
	    (i.e. of the required CSSfilename) 
	   (the tests below are in essence the logic of "our media queries"
	    ...except expressed in a more succinct way and as Javascript) */
	if (scrwid >= k[0]) l = 1;
	if (scrwid >= k[1]) l = 2;
	if (scrwid >= k[2]) l = 3;
	if ((scrwid >= k[3]) && (scrasp >= 1.67)) l = 3;

	/* create the correct HTML and insert it into the DOM
	    (this could also be done with the old -obsolete?- "document.writeln") */
	if (l) {
		var headEls = document.getElementsByTagName('head');
		var newEl = document.createElement('link');
		if (headEls && newEl) {
			/* if the DOM is not fully supported, we can't do this so easily,
			    so instead just fail silently and present the "default" style */
			newEl.type = 'text/css';
			newEl.rel = 'stylesheet';
			newEl.href = ['','tablet','desktop','widescreen'][l] + '.css';
			headEls[0].appendChild(newEl);
		}
	}
</script>
</head>
<body>
<!--   ...   -->
</body>
</html>

Caveat - Unchanging Properties Only

Some of the properties that Media Queries can test may change as the user manipulates the browser. For example window size and device orientation (portrait or landscape) may change. Other properties that Media Queries can test don't ever change on a given device. They are properties of the device itself. For example screen size and pixel density never change.

The above specific example (and probably this whole technique) is applicable only to properties that never change (i.e device properties). Since it works only with unchanging properties, it can correctly be done only once before the page displays, then never done again.

Media-Query-like tests of properties that might change must be redone every time the browser signals a possible shift (i.e whenever it fires the onresize event). It's not immediately clear how to modify the specific example above so it can be redone. In fact, it's not even clear this alternative to Media Queries can be used at all with properties that might change.

(I don't find this to be a significant limitation, becase in my opinion responsive design should only be testing and responding to unchanging/device properties anyway. I personally think if the page layout changes when the user grows or shrinks the window on the same device, something is wrong. But not everyone shares that opinion. And no such limitation is intrinsic to the way CSS Media Queries are specified or implemented.)

Sequence of Events

The desired sequence of events, where each step completes before the next one begins, is:

  1. Execute Javascript to select CSS.
  2. Load and/or activate the selected CSS.
  3. Go through the HTML <body>, parsing it, laying it out, constructing the DOM tree, and painting (displaying) it on the screen.

To force events to hew to this sequence, yet not reduce performance terribly, requires a few tricks. These are all done by the specific example above, but that may not be immediately obvious:

(Note that a few alternate sequences of events will seem to more or less work anyway. These cases are ultimately undesirable and should be avoided because the user will see a flash of unstyled or incorrectly styled content when the page first loads.)


Location: N42 40.86' W070 50.35'
 (North America> USA> Massachusetts> Boston> Metro North> Ipswich)
Email comments to Chuck Kollars
Time: UTC-5 (USA Eastern Time Zone)
 (UTC-4 summertime --"daylight savings time")

All content on this Personal Website (including text, photographs, audio files, and any other original works), unless otherwise noted on individual webpages, are available to anyone for re-use (reproduction, modification, derivation, distribution, etc.) for any non-commercial purpose under a Creative Commons License.