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.

10 Comments Post your own

  1. Dennis

    I don’t see the point to trig­ger event on medi­a­que­ries browser change. Isn’t match­Me­dia enough to spec­ify dif­fer­ent js behav­iour from mediaqueries ?

    Not sure if users often (not say­ing never) resize their browser and expect for change in interface.

    But maybe i miss some­thing from mis­trans­la­tion so i’ll be glad to hear your thoughts on this.

  2. Simon

    I don’t agree with Den­nis and think this codes are use­ful for sure and worth check­ing out.

    Thanks for sharing!

    To reply to Den­nis;
    It’s not only for the user’s expec­ta­tion, but to opti­mize both the User Expe­ri­ence and screen / space usage.

  3. Horia Dragomir

    I think this is great!

    Also you can eas­ily use this to deter­mine other aspects of the cur­rent nav­i­ga­tor, like devicePixelRatio.

    It may also solve some view­port prob­lems on handhelds.

  4. Geert-Jan

    @Dennis: it’s indeed needed for resize and resize alone. Also think of an iPhone user rotat­ing his device from por­trait to land­scape, etc.

    I think it’s great to have this last part of the cir­cle almost complete.

  5. Marc Finnern

    Nice Work!

  6. Stefan

    I’m find­ing your blog post really hard to read as the font is so thin it doesn’t dis­play very well. And the com­ments sec­tion is even more com­pli­cated as its light grey on black which offers no contrast.

    Sorry to be a pain, but your putting up bar­ri­ers to the content.

  7. Brekk

    Wow! That’s really excit­ing — it offers a wealth of pos­si­bil­i­ties in the sup­ported browsers. Thanks for posting.

  8. stephband

    If you use CSS’s ‘con­tent’ prop­erty, you only need to test one ele­ment, rather than one ele­ment per media query. Some­thing like this:

    @media all and (min-width: 42em) {
    #mq {
    con­tent: ‘min-width: 42em’
    }
    }

    @media all and (min-width: 60em) {
    #mq {
    con­tent: ‘min-width: 60em’
    }
    }

    You’ll find that all browsers can read the con­tent prop­erty, even if it is not dis­played on screen.

  9. stephband

    Of course, I left out the tran­si­tions in the exam­ple above, to keep it clear.

  10. Paul Hayes

    @stephband: That assumes the media queries are mutu­ally exclusive.