Enquire.js – Media Query Callbacks in JavaScript

Published by Guest Author

The following is a guest post Nick Williams. Nick emailed me about a new library he created to assist in working with responsive design. Specifically, he was implementing an idea he found here on CSS-Tricks but wanted to do so more intelligently by only running the script when needed. Thus the birth of this new mini library! There are some kind of similar ones out there, but this one is very small and works with native media queries. I'll let Nick tell you more about it...

Enquire.js is a JavaScript library I created for dealing with media queries in JavaScript.

"Whoa! Back it up! Media queries in JavaScript?! What kind of abomination is this?"

Whilst the premise may seem odd, it really is a handy addition to the responsive toolbox.

The Lowdown

Enquire.js is a lightweight, pure JavaScript library for handling media queries. It is less than 1kb when GZIP'd and minified, and has absolutely no dependencies. Yup, you read that right - no dependencies, not even jQuery! The most you will have to do is provide a matchMedia polyfill if you wish to support browsers without a native implementation.

Use Cases

  • Responsive Design
  • Mobile-first Design
  • Assist older browsers with media queries

Raison D'etre

So how and why did this come about? I think the easiest way to explain is through a simple scenario...

Imagine you've got an established website, currently fixed-width and targeted at large-screened devices. You've heard all the hubbub about this "responsive design" business and wish to get in on some of the action. So you dutifully start designing and planning how your website may work on small-screened devices. You've cut out all the chuff, cleared your floating sidebar and bumped up the size of your form elements. Great!

But what about that pesky menu? It's beefy, it's got multiple levels and relies on CSS hover effects to expose the deeper hierarchy - in other words, it ain't gonna work on a small-screened touch device!

So you do a little digging around on Google and stumble across a clever solution to convert a menu into a dropdown. High fives all round, your menu is now functional on mobile touch-based devices! But you've got this nagging sensation. You're not quite sure what it is, but something's still not quite right. Ah yes, that's it; you're running this code even for large screened devices, even though they have absolutely no need for it! You shed a little tear for the wasted CPU cycles, wishing that there was a way to selectively run this JavaScript.

A New Challenger Appears!

This is where enquire steps in to help out.

For now, we'll run with the converting a menu to a dropdown scenario, making the menu appear when the window is less than 960px. With enquire, it's as simple as this:

enquire.register("max-width: 960px", function() {
  // put Chris' code here to convert your menu to a dropdown
});

All this does is tell enquire to invoke the supplied function only if the given media query matches. And so, you save those precious CPU cycles for large-screened devices. OK, I realise that's a bit silly since mobile devices typically have lesser-powered CPUs compared to larger screened devices, but it works from the other direction also - the much-fabled mobile first approach!

Digging Deeper

At this point it's worth quickly running through the enquire API before we get into some meatier scenarios. The previous example showed the most simple use-case for enquire - supplying a function as the second parameter to be run when a media query matches. This will suffice in most scenarios, however enquire can do a lot more if you've got more demanding requirements. You can pass the register method an object as so:

enquire.register("screen and (max-width: 1000px)", {

  // REQUIRED
  // Triggered when the media query transitions
  // from *unmatched* to *matched*
  match : function() {},
  
  // OPTIONAL   
  // Triggered when the media query transitions 
  // from a *matched* to *unmatched*                        
  unmatch : function() {},    
                                
  // OPTIONAL
  // Triggered once immediately upon registration of handler
  setup : function() {},      
                                
  // OPTIONAL
  // Defaults to false
  // If true, defers execution of the setup function
  // until the first media query is matched (still just once)
  deferSetup : true           
});

This signature gives you full control and allows you to define exactly what you want to happen, and when you want it to happen. This only really becomes useful when you make a call to enquire's listen function (more on this shortly).

Examples

So let's get onto some examples that show the more advanced features in action.

Faux Media Queries In Legacy Browsers

Enquire.js can be used to fake some rudimentary media queries in older browsers. However, please don't use this approach as a replacement to genuine media queries in your CSS, just use it as a monkey patch for older browsers (jQuery used in this example):

$(function() {

  // cache body for speed
  var $body = $("body");

  // DRY up handler creation
  function handlerFactory(className) {
    return {
      match : function() {
        $body.addClass(className);
      },
      unmatch : function() {
        $body.removeClass(className);
      }
    };
  }

  // hook up our "media queries"
  enquire
    .register("screen and (max-width : 320px)", handlerFactory("lt-320"))
    .register("screen and (max-width : 640px)", handlerFactory("lt-640"))
    .listen();

});

Here we're using match and unmatch handlers to add or remove classes to the <body> element, which allows us to target styles at different sized screens. Notice we can chain calls to all of enquire's functions.

Of particular note here is the call to the listen function so that enquire will respond to browser resize and orientation change events (though of course in this example, orientation change events are unlikely in legacy browsers). For optimal performance, listen is throttled to only respond to one event in a given timeframe. By default this is 500ms, but you can specify your own throttle by passing as a parameter to listen:

// 10000 milliseconds = 10 seconds. Why not?!
enquire.listen(10000); 

Dogfooding

Let's walk through an example of a real world use of enquire, taken from the enquire.js project page (dogfooding FTW!). Here I wanted to make the page load as quick as possible by only loading the bare minimum amount of JavaScript by default, opting to load anything beyond that asynchronously, only when required. This is so that small-screened devices (and by proxy those more likely to be on a slow connection) get the speediest page load possible, and only large-screened devices have to deal with the extra resources.

In particular, this pattern is used to pull all code required to generate and render the "Jump To" sidebar of the project page. This example is slightly simplified as not to distract from the important concepts:

$(function() {

  $toc = $(".toc");

  enquire.register("screen and (min-width: 1310px)", [{

    deferSetup : true,

    setup : function () {

      // use Modernizr's yepnope to load 
      // additional scripts and manage callbacks
      Modernizr.load([ 
        {
          load: "js/jquery.toc.js",
          callback : function() {
            // code to generate "Jump To" list
            // and append to body
          }
        },  

        // load in the bootstrap plugins.
        // they hook themselves up via their data-API
        "js/vendor/bootstrap-scrollspy.js",
        "js/vendor/bootstrap-affix.js"
      ]);
    },

    match : function() {
      $toc.fadeIn();
    },

    unmatch : function() {
      $toc.hide();
    }

  }]).listen();   

});

Two new options have been introduced here - setup and deferSetup. setup is a function that gets called just once, which is great for getting all your expensive DOM manipulation out of the way up-front. By default, setup is invoked as soon as you register your query handler, regardless of whether the query has matched yet. However, if you supply the deferSetup flag we can postpone setup until the first time a query is matched.

In this case it obviously makes sense, as scripts only be loaded for large-screened devices. The match and unmatch function are then left to simply show and hide the "Jump To" list.

Other Examples

A few more scenarios which enquire would handle effortlessly:

  • Responsive images
    • Use data-* attributes and switch an image's src depending on which media query matches
  • Shuffle content around
    • Sometimes it's not as easy as just stacking items on small screens, you may want content to appear in a completely different order.
    • Again, I'd recommend a declarative approach with data-* attributes so you can pick out source elements and destination elements.

There's More...

Enquire.js still has more to offer! You can register multiple handlers per query, and also multiple queries, meaning you've got ultimate flexibility! To read more about this, please visit the enquire.js project page.

Download, Fork and Contribute!

For downloads and source code, visit the project on github. If you wish to contribute, please do so, I'd love other people's opinions, thoughts and ideas. Fork to your heart's content and make a pull request with any changes. If you encounter any issues, feel free to create an issue on github and I will duly fix the problem :)

Licence

The project is licenced under the MIT licence, so you're free to use it in any way you wish.