Device State Detection with CSS Media Queries and JavaScript

By  on  

Being able to detect device state at any given moment is important for any number of reasons and so it's important that web app CSS and JavaScript are in sync with each other. In working on the Mozilla Developer Networks' redesign, I found that our many media queries, although helpful, sometimes left JavaScript in the dark about the device state. Is the user viewing the site in desktop, tablet, or phone screen size? Easy from a CSS perspective but CSS doesn't directly speak with JavaScript. I've created a system based on media queries and z-index which can tell me which media query the user is viewing the site in at any given time, so that I can make adjustments to dynamic functionality whenever I want!

The CSS

The first step is creating media queries important to your application and goals. For the sake of an example, we'll create three new media queries (not including the default "all") which will accommodate four states: desktop (default, doesn't require a media query), "small desktop", tablet, and phone. With each of those states, we'll set a different z-index on an element we'll use to detect the state. The element will be positioned well offscreen so that's not visible; remember, it's only purpose is holding a z-index value that we can retrieve via JavaScript:

/* default state */
.state-indicator {
    position: absolute;
    top: -999em;
    left: -999em;

    z-index: 1;
}

/* small desktop */
@media all and (max-width: 1200px) {
    .state-indicator {
        z-index: 2;
    }
}

/* tablet */
@media all and (max-width: 1024px) {
    .state-indicator {
        z-index: 3;
    }
}

/* mobile phone */
@media all and (max-width: 768px) {
    .state-indicator {
        z-index: 4;
    }
}

Each of those z-index numbers will indicate to our JavaScript code that we're in a given device size at that time. We aren't trying to detect that the user is giving a given device, as the user could simply have their desktop window in a narrow state, but it does give us information about screen real estate for the sake of our web app layout.

The JavaScript

You'll likely want to know the screen size upon DomContentLoaded but since you may want to query for it at any time (since the user could resize their window), we'll require a function be called to get the state any time it is requested:

// Create the state-indicator element
var indicator = document.createElement('div');
indicator.className = 'state-indicator';
document.body.appendChild(indicator);

// Create a method which returns device state
function getDeviceState() {
    return parseInt(window.getComputedStyle(indicator).getPropertyValue('z-index'), 10);
}

So let's say you want to usage this system to determine if a widget should initially display or should be hidden:

if(getDeviceState() < 3) { // If desktop or small desktop
    // Show the widget....
}

One could argue that relying on those number keys could be confusing or hard to maintain, so you could use an object to deal with that:

function getDeviceState() {
    var index = parseInt(window.getComputedStyle(indicator).getPropertyValue('z-index'), 10);

    var states = {
        2: 'small-desktop',
        3: 'tablet',
        4: 'phone'
    };

    return states[index] || 'desktop';
}

In this case, you could create more English-friendly conditionals:

if(getDeviceState() == 'tablet') {
    // Do whatever
}

Possibly the better option is using pseudo-element content with CSS and JavaScript :

.state-indicator {
    position: absolute;
    top: -999em;
    left: -999em;
}
.state-indicator:before { content: 'desktop'; }

/* small desktop */
@media all and (max-width: 1200px) {
    .state-indicator:before { content: 'small-desktop'; }
}

/* tablet */
@media all and (max-width: 1024px) {
    .state-indicator:before { content: 'tablet'; }
}

/* mobile phone */
@media all and (max-width: 768px) {
    .state-indicator:before { content: 'mobile'; }
}

That key could then be retrieved with this JavaScript:

var state = window.getComputedStyle(
    document.querySelector('.state-indicator'), ':before'
).getPropertyValue('content')

How you organize this code is also up to you. If you have one global object where you pin methods and properties (like a window.config or window.app global or similar), you can pin the method on that. I prefer using AMD format modules but to each their own. You could add it as a plugin to jQuery or whichever JavaScript library you use. Regardless of how you implement, you now have reliable, easy to use device state detection on the client side thanks to media queries!

Furthering the Effort

We know that screen resizes happen, whether manual window resizing on desktop or via orientation change on mobile devices, so we may want some type of event system to announce those changes when they occur. That's as simple as you would expect:

var lastDeviceState = getDeviceState();
window.addEventListener('resize', debounce(function() {
    var state = getDeviceState();
    if(state != lastDeviceState) {
        // Save the new state as current
        lastDeviceState = state;

        // Announce the state change, either by a custom DOM event or via JS pub/sub
        // Since I'm in love with pub/sub, I'll assume we have a pub/sub lib available
        publish('/device-state/change', [state]);
    }
}, 20));

// Usage
subscribe('/device-state/change', function(state) {
    if(state == 3) { // or "tablet", if you used the object

    }
});

Note that I've used function debouncing to limit the rate at which the resize method is fired -- that's incredibly important for the sake of performance. Whether you use pub/sub or custom DOM events is up to you, but the point is that creating a state change listener is easy!

I love this system of resize and device state management. Some will point out matchMedia as an option but the problem with that is needing to have the media queries in both the CSS and the JavaScript and since media queries can be complex, that seems like more of maintenance nightmare than simply using z-index codes. People could argue than one could use window.innerWidth measurements but that's simply trying to translate media queries to JS conditionals and that's a nightmare too. What's also nice about this is that you can use the same type of system for any type media query signifier, like checking for portrait or landscape orientation.

In any event, give it a try and let me know what you think!

Recent Features

Incredible Demos

  • By
    Create a Download Package Using MooTools Moousture

    Zohaib Sibt-e-Hassan recently released a great mouse gestures library for MooTools called Moousture. Moousture allows you to trigger functionality by moving your mouse in specified custom patterns. Too illustrate Moousture's value, I've created an image download builder using Mooustures and PHP. The XHTML We provide...

  • By
    MooTools Wall Plugin

    One of the more impressive MooTools plugins to hit the Forge recently was The Wall by Marco Dell'Anna.  The Wall creates an endless grid of elements which can be grabbed and dragged, fading in elements as they are encountered.  Let me show...

Discussion

  1. Daniel

    Also modernizr can run mediaqueries so you can do something like this:

    Modernizr.mq(‘only screen and (min-width: 960px)’)

    • Which is fine but you have to duplicate the media query in CSS and JS — seems like a maintenance nightmare to me.

  2. Jayapal Chandran

    matchMedia function did the trick for me. It provides event listener for screen size change. But it will not work in IE < 10.

    • Same issue: you’re still having to keep the media queries in multiple spots, the JS and the CSS.

  3. Hi David,

    I’ve done the same thing but with the height of an element instead of z-index.

    It seems that there are some problems with z-index and some mobile browsers.
    http://caniuse.com/#search=z-index http://css-tricks.com/almanac/properties/z/z-index/

    Do you know if a partial support of z-index can cause problems with values we get with js ?

    Could “z-index screw-up complex positionning” -> http://adactio.com/journal/5429/
    (the :after content explained there doesn’t work with some browsers : android2.1 at least )

    I also used one more value (with the element height) :

    the default value was to detect browsers that don’t support MQ at all
    the first value (with a min-width of some px ) to detect the support of MQ

  4. MaxArt

    Yeah, but do you know what’s a maintenance nightmare? Magic numbers.

    And your trick, although interesting, not only pollutes the DOM and the stylesheets, but it’s entirely based on magic numbers.

    Not gonna use it as it is, but I think it can be improved.

  5. I’ve used something very similar but expanding upon the technique that Jeremy Keith wrote up here: http://24ways.org/2011/conditional-loading-for-responsive-designs and more related info here: http://seesparkbox.com/foundry/breakpoint_checking_in_javascript_with_css_user_values. Brett Jankord also made a project of it on GitHub here: https://github.com/bjankord/Media-Query-Sync

    My concern with your approach (it’s minor I concede) is that the UA is drawing a box somewhere offscreen (kind of like the old text hide approach). With the pseudo class method the UA doesn’t have to draw anything.

    Is there a specific reason you went for z-index as the flag?

  6. The style property only returns inline styles, and doesn’t work in this case. I made a small demo how to get the z-index in this case which works cross-browser:

    http://codepen.io/CrocoDillon/pen/DqLFn

  7. Nice solution.
    I like it.

  8. Fil79

    Awesome thanks! but It’s kind of disappointing to not get a download link, mostly for learning porpoises.

  9. castus

    Brilliant solution, I like it, thanks!

  10. Loupax

    Great article! The only thing that confused me is that there is no reference on what the debounce() function do or how it works, causing someone who haven’t heard of it think this is a native javascript function

    Here it is for those who look for it :) http://davidwalsh.name/function-debounce

  11. Wouldn’t a lookup table be better than a switch statement?

    var getDeviceState = {
      "1": function() {
        return "small-desktop";
      },
      "2": function() {
        return "tablet";
      },
      "3":  function() {
        return "smartphone";
      }
    };
    
    • That could be shortened, but that’s another option.

  12. Interesting to see that nobody has raised the issue where you’re using classes and not ids? Wouldn’t it be more performant to just create the div element with a specific id that can be referenced in both the css and js?

    I’ll get my coat.

  13. Cid

    Just came across this nifty little idea (I think it was in a presentation by Swen Wolfermann). Anyway It’s the same principle, but somehow more simple and it lets you name your media query hooks in CSS directly and skip the numeric translation in js – just use the :after content of some element (why not the body?) to store info about your media query, instead of creating ghost elements.

    /*CSS*/
    body:after {
    	  content: 'fluid';
    	  display: none;
    	}
    @media only screen and (min-width: 64em) {
    	  body:after { content: 'fixed'; }
    	}
    
    /*JS*/
    var size = window.getComputedStyle(document.body,':after').getPropertyValue('content')
    
  14. I did it too, before you! ;-P

    http://www.tighelory.com/2013/08/dynamic-javascript-based-on-css-media.html

    I used hidden objects, I like that you used a z-index though.

  15. I set up a webpage with a Desktop.Css and Mobile.Css but I don’t know How to put it together one I have set up as Index.html and the other as m.index.html how can I get it set up where one is for Desktop/laptop and the other for mobile

  16. Luis Herrero

    Another cool solution is add a pseudoselector to the body tag, change content via media queries so you can read it in JavaScript.

    • This approach is a brilliant idea. But using a pseudoelement on body may cause problems because some other application may use it for something else. I prefer to create an empty span with unique id for that to avoid conflicts.

  17. Luis Herrero

    Yep, like Cid example. Didn’t saw the comment :(

  18. Look also Apache Mobile Filter is a free Open Source Project, and is “de facto” the standard to detect mobile devices.

  19. I’m using this technique since a couple of month now and it’s fantastic. But I’m using the z-index to indicate the device width. E.g. z-index: 768. https://plus.google.com/114945019104499996712/posts/dW7DrHifryB

  20. Hi,

    I’ve been working on a javascript module for client detection which has been donated to the Apache DeviceMap project (still incubating, but we’re getting close to graduation).

    The module’s called BrowserMap – you can check out the code at https://github.com/apache/devicemap-browsermap.

    Let me know what you think!

  21. Andy Foster

    This is nice. But I went the other way around.

    I went with mobile as the default and expanded from there.

    I used this: http://www.html5rocks.com/en/mobile/responsivedesign/

    as a starting point. Thought it worked better this way for me.

    Regardless this post is great and def would incorporate some of your insight.

  22. I think using faux elements is a neat trick. I just did similar and this confirmed my “theory” (cause at first it felt kinda dirty / odd). Thanks.

    That said, wouldn’t it be best to put that element’s CSS inline just to make sure it’s always off screen? Minor? Yup. But know the less safer, yes?

  23. p.s. Again, I think this is great. But then again, kinda a hack in a way. Should n’t this be something that is native? When are we going to have that discussion?

    Thanks again. I’ll take my answer off the air :)

  24. Adebola

    So helpful, You saved me a great deal with this hack.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!