Saturday, December 07, 2013

Device State Detection with CSS Media Queries and JavaScript

Device State Detection with CSS Media Queries and JavaScript

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 implement a switch to deal with that:

function getDeviceState() {
    switch(parseInt(window.getComputedStyle(indicator).getPropertyValue('z-index'), 10)) {
        case 2:
            return 'small-desktop';
            break;
        case 3:
            return 'tablet';
            break;
        case 4:
            return 'phone';
            break;
        default:
            return 'desktop';
            break;
    }
}

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

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

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 switch

    }
});

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.