Making Sass talk to JavaScript with JSON

Published by Guest Author

The following is a guest post by Les James. Like many of us, Les has been gunning for a solution to responsive images that works for him. In this article he shares a technique he found where he can pass "named" media queries from CSS over to the JavaScript, which uses those names to swap out the image for the appropriate one for that media query. And even automate the process with Sass. HTML purists may balk at the lack of true src on the image, but at this point we gotta do what we gotta do.

Current proposed responsive image solutions require that you inline media query values into HTML tags.

<picture>
    <source media="(min-width: 45em)" src="large.jpg">
    <source media="(min-width: 18em)" src="med.jpg">
    <source src="small.jpg">
    <img src="small.jpg" alt="">
</picture>

This is a problem for me. I like my media queries in one spot, CSS. The above example doesn't feel maintainable to me, and part of that might be due to my approach to RWD. I use the Frameless grid approach to creating layouts. I tell Sass how many columns I want in a layout and it generates a media query to fit it. What this means is that I only think in column counts and I actually have no idea what the actual values of my media queries are.

When adapting this approach to responsive images, I needed a way to provide meta data around my breakpoints. So when I create a breakpoint, I give it a label. I've recently been a fan of labels like small, medium and large but the labels could be anything you want. The point is that you name your breakpoints with something meaningful. The goal is to pair those names to matching sources defined in my HTML. The first step is to format our label into something JavaScript can parse.

Sass to JSON

When I create a breakpoint I call upon a Sass mixin I created. The column count creates a min-width media query to fit that column count. The label gives a name to our media query.

@include breakpoint(8, $label: "medium") {
    /* medium size layout styles go here */
}

The breakpoint mixin passes that label to a function which formats it into a string of JSON.

@function breakpoint-label($label) {
    @return '{ "current" : "#{$label}" }';
}

JSON to CSS

Now that we have our label converted to JSON, how do we get it into our CSS? The natural fit for a string is CSS generated content. I use body::before to hold my string because it's the least likely spot for me to actually use for display on the front end. Here is how the label finds its way into CSS from my breakpoint mixin.

@if($label) { body::before { content: breakpoint-label($label); } }

Unfortunatly I have to support older browsers and they will have trouble reading our CSS generated content with JavaScript. So we have to place our JSON in one more spot to gain further compatibility. For this I'm going to add our JSON as a font family to the head.

@if($label) {
    body::before { content: breakpoint-label($label); }
    .lt-ie9 head { font-family: breakpoint-label($label); }
}

CSS to JS

Our layout label is now sitting in a JSON string in our CSS. To read it with JavaScript we turn to our friend getComputedStyle. Let's create a function that will grab our JSON and then parse it.

function getBreakpoint() {
    var style = null;
    if ( window.getComputedStyle && window.getComputedStyle(document.body, '::before') ) {
        style = window.getComputedStyle(document.body, '::before');
        style = style.content;
    }
    return JSON.parse(style);
}

For browsers that don't support getComputedStyle we need to throw in a little polyfill and grab the head font family instead.

function getBreakpoint() {
    var style = null;
    if ( window.getComputedStyle && window.getComputedStyle(document.body, '::before') ) {
        style = window.getComputedStyle(document.body, '::before');
        style = style.content;
    } else {
        window.getComputedStyle = function(el) {
            this.el = el;
            this.getPropertyValue = function(prop) {
                var re = /(\-([a-z]){1})/g;
                if (re.test(prop)) {
                    prop = prop.replace(re, function () {
                        return arguments[2].toUpperCase();
                    });
                }
                return el.currentStyle[prop] ? el.currentStyle[prop] : null;
            };
            return this;
        };
        style = window.getComputedStyle(document.getElementsByTagName('head')[0]);
        style = style.getPropertyValue('font-family');
    }
    return JSON.parse(style);
}

There is a major problem with our function right now. Our JSON is passed as a string which means that it is wrapped in quotes, but what kind of quote depends on which browser you are using. WebKit passes the string wrapped in single quotes. Firefox passes the string wrapped in double quotes which means that it escapes the double quotes inside of our JSON. IE8 does something really wierd and adds a ; } to the end of our string. To account for these inconsistancies we need one more function to normalize our JSON before we parse it.

function removeQuotes(string) {
    if (typeof string === 'string' || string instanceof String) {
        string = string.replace(/^['"]+|\s+|\\|(;\s?})+|['"]$/g, '');
    }
    return string;
}

Now before parse the JSON in the return of our getBreakpoint function we just pass the string through our removeQuotes function.

return JSON.parse( removeQuotes(style) );

Image Source Matching

JavaScript can now read the label that we defined for each breakpoint. It's trivial at this point to match that label to a responsive image source. Take the following image for example.

<img data-small="small.jpg" data-large="large.jpg">

When the active media query is small, we can have JavaScript match that to data-small and set the source of our image to small.jpg. This works great if you've declaired a source for every breakpoint but as you can see in our example we don't have a source defined for medium. This is a very common scenario. The small image can typically work in larger layouts. Maybe the medium layout just added a sidebar and our image size didn't change. So how does JavaScript know to pull the small source when the layout is medium? For this we need ordering.

Sass List to JavaScript Array

Every time we create a breakpoint we can store that label in a Sass list. In our breakpoint mixin we can append the label to our list.

$label-list: append($label-list, $label, comma);

Assuming that our list defaults to a pre-populated mobile label of small the following breakpoints will create a list of small, medium, large.

@include breakpoint(8, $label: "medium");
@include breakpoint(12, $label: "large");

The order you declare your breakpoints in Sass will determine the order of your labels. Let's add this Sass list as an array to our JSON function.

@function breakpoint-label($label) {
    $label-list: append($label-list, $label, comma);
    @return '{ "current" : "#{$label}", "all": [#{$label-list}] }';
}

Now in addition to JavaScript knowing the current breakpoint label it can look at the array we passed and know their order. So if the layout is medium and no matching data attribute is found on our image, JavaScript can find medium in our label array and walk backwards through that array until a matching source is found.

In Summary

This responsive image solution does something really important for me. By tagging media query values with labels I create flexibility and simplicity. If I change the size of a breakpoint I only have to change it in one place, my Sass. My HTML image sources aren't dependent on the value of my media queries, just their name.

Although I've accomplished this with Sass, it's actually not necessary. You can manually write your JSON string into your media queries, Sass just helps automate the process. Here is a simple Pen that uses pure CSS.

Check out this Pen!

To see a more robust, production quality implementation of this technique check out the code behind my framework Breakpoint.

Please note, if you want your image tags to be valid then stub them with a source like src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D" and please, make sure you provide a <noscript> fallback.

My hope for sharing this is that it spurs further discussion and ideas around abstracting the complexities and maintainability of responsive images. I'm curious to hear your thoughts and if you have ways to improve this.

We all stand on the shoulders of giants. Much credit and inspiration for this is due to Jeremy Keith's post Conditional CSS and Viljami Salminen's detectMQ.

More on Sass & JSON Working Together

  • Hugo Giraudel has a library for encoding and decoding JSON from Sass.
  • Robert Balicki has a library for turning JSON into Sass variables.