Use CSS transitions to link Media Queries and JavaScript

A com­mon prob­lem in respon­sive design is the link­ing of CSS3’s media queries and JavaScript. For instance on a larger screen we can restyle, but it might be use­ful to use JavaScript and pull in dif­fer­ent con­tent at the same time, e.g. higher qual­ity images. With CSS tran­si­tions, specif­i­cally their tran­si­tio­nEnd events, we can marry up our media queries and JavaScript per­fectly, with­out resort­ing to win­dow resize events.

We need a way of test­ing media query rules in JavaScript, and a way of gen­er­at­ing events when a new rule matches. There’s a spec­i­fi­ca­tion for exactly this: there’s match­Me­dia to see if a query matches, and Medi­a­Que­ryList with Medi­a­Que­ryLis­ten­ers to detect and respond to changes.

match­Me­dia has sup­port in Chrome, Fire­fox 6+ and Safari 5.1+ and there’s even a poly­fill (by Scott Jehl, Paul Irish, Nicholas Zakas) for other browsers. So we can hap­pily per­form our one off tests in JavaScript (prob­a­bly on page load):

if (matchMedia('only screen and (max-width: 480px)').matches) {
  // iphone specific JS
}

How­ever media query list lis­ten­ers (that’s a mouth­ful) are only sup­ported in Fire­fox 6+. This is the magic of fir­ing events when some­thing changes, it com­pletes the circle.

All is not lost, there is another way of using CSS to gen­er­ate events and that’s through CSS tran­si­tions. When a CSS tran­si­tion ends it fires a tran­si­tio­nEnd event (webkit­Tran­si­tio­nEnd, oTran­si­tio­nEnd, tran­si­tio­nend) on the appro­pri­ate ele­ment. Now con­sider a tran­si­tion that occurs only when a media query is applied. Hey presto, we have a JavaScript event trig­gered by media query rules, how­ever com­pli­cated or con­vo­luted that rule may be. This rocks and can form the basis of a Medi­a­Que­ryList polyfill.

Sim­ple tran­si­tion CSS and event listener

.mq {
-webkit-transition: width 0.001ms;
-moz-transition: width 0.001ms;
-o-transition: width 0.001ms;
transition: width 0.001ms;
width: 0;
}

@media all and (max-width: 480px) {
.mq {
width: 1px;
}
}
var mq = document.querySelectorAll('.mq')[0],		
mq.addEventListener('webkitTransitionEnd', function() {
	/* Transition ends, media query matched */
}, false);

Proof of concept

I’ve taken the excel­lent match­Me­dia poly­fill and quickly hacked together a ver­sion that includes tran­si­tion events and a callback.

Proof of con­cept demo
(and on Github)

mql('all and (max-width: 700px)', callback);

Pass in a media query string and call­back. It imme­di­ately returns the test result and the call­back will fire when­ever the test result changes, the only argu­ment being the Medi­a­Que­ryList object. This isn’t a poly­fill because it doesn’t yet match the spec­i­fi­ca­tion, if it did the orig­i­nally returned MQL object would have addLis­tener and removeLis­tener func­tions (that’s a work in progress).

The CSS tran­si­tions are instan­ta­neous via a dura­tion of a lit­tle larger than 0, I’ve cho­sen 0.001ms.

Ele­ment tran­si­tions are bidi­rec­tional, so the event fires when the rule matches and when it no longer matches. Every time the event fires a test is per­formed to deter­mine state, this is easy using matchMedia.

The CSS tran­si­tion event tells us which ele­ment trig­gered the tran­si­tion but no details about the media query rules that gov­erned it. So we use unique ele­ments for each rule to con­nect the dots.

.mq {
-webkit-transition: width 0.001ms;
-moz-transition: width 0.001ms;
-o-transition: width 0.001ms;
transition: width 0.001ms;
width: 0;
}
mql = (function(doc, undefined){

  var bool,
      docElem  = doc.documentElement,
      refNode  = docElem.firstElementChild || docElem.firstChild,
      idCounter = 0;

  return function(q, cb){
	
	var id = 'mql-' + idCounter++,
	    callback = function() {
		cb({ matches: (div.offsetWidth == 42), media: q });
	    },
	    div = doc.createElement('div');
	
	div.className = 'mq';
	div.style.cssText = "position:absolute;top:-100em";
	div.id = id;
        div.innerHTML = '&shy;<style media="'+q+'"> #'+id+' { width: 42px; }</style>';

	div.addEventListener('webkitTransitionEnd', callback, false);
	div.addEventListener('transitionend', callback, false); //Firefox
	div.addEventListener('oTransitionEnd', callback, false); //Opera

        docElem.insertBefore(div, refNode);
        //don't delete the div, we need to listen to events
        return { matches: div.offsetWidth == 42, media: q };
  };

})(document);

Demo code

$(function() {

    var $dynamic = $('.dynamic');
    mql('all and (max-width: 700px)', change);
    mql('all and (max-width: 500px)', change);
    mql('all and (min-width: 1200px)', change);

    function change(mql) {
        console.log(mql);
        $dynamic.prepend('<p>' + mql.media + ' &mdash; ' + mql.matches + '</p>');
    }
});

Sup­port

Obvi­ously for this to work we need both CSS tran­si­tions and media query sup­port in the browser. Look­ing at caniuse​.com and QuirksMode this tech­nique should be sup­ported by: Chrome, Android (2.1+), Opera (10.6+), Opera Mobile (10.0+), Fire­fox (4+), Safari (3.2+) and iOS (3.2+). Samsung’s Dolfin and Black­berry 6 sup­port CSS ani­ma­tions, I pre­sume that means tran­si­tions too, but I can’t eas­ily test.

Of course, IE is lag­ging behind, as always. IE9 sup­ports media queries but it has no tran­si­tion sup­port, that’s com­ing in IE10.

Paul Hayes

Paul Hayes is a developer at Last.fm. You should follow him on Twitter, where he talks about UX, HTML, CSS and JavaScript, amongst other cool stuff.