Grow your CSS skills. Land your dream job.

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.

Comments

  1. That’s a very clever technique man. Seems like a lot of work to get it to work, all the messing around with JSON for different browsers, ugh. Ain’t nobody got time for that! haha
    Just messing, great technique.

    • Les James
      Permalink to comment#

      Thanks Pedro. Using this on projects is really easy. It was a lot of work to get to this point though.

  2. D. Burger
    Permalink to comment#

    Great technique. I like the SASS part. More fuel for the responsive images techniques.

    I developed Responsive-Fragments (on Github) using a similar technique, but instead it Ajax loads in content (HTML fragments) matching a certain MediaQuery Breakpoint ‘keyword’ or label. Using a HTML data-attribute that matches to a HTML fragment URL.

    <div data-device-tablet="/fragments/sidebar.html"></div>

    This also uses a fallback (array like) system to load in subsequent HTML fragments when on desktop (loads in smartphone, tablet AND desktop HTML fragments). This is great when loading in different HTML based on the device MediaQuery and therefore optimizing bandwidth / images etc.

    This was also inspired by Jeremy Keith’s article.

    Kind regards from the Netherlands.

  3. That’s an impressive technique. Thanks for sharing.

  4. Hjörtur
    Permalink to comment#

    Very interesting indeed!

    We used a similar approach to make our Css/Sass/Less trigger breakpoints in Javascript, built a library around it on github: https://github.com/14islands/js-breakpoints

  5. Am I missing something or browsers that don’t support generated content also don’t support Media Queries? (I’m talking about IE8).

    • Les James
      Permalink to comment#

      Since the desktop layouts I create are wrapped in media queries I create a duplicate layout sandboxed with a lt-ie9 class. The <head> font-family code gets added to that lt-ie9 class.

    • Paul d'Aoust
      Permalink to comment#

      Aha, I was wondering the very same thing. Thanks for the info — so you have sort of an unresponsive version of the desktop layout, and the JavaScript also needs to be able to find out what styles are applied in those old browsers too, right? That makes sense, and I don’t think I would’ve thought of that myself.

      (Great technique, by the way. I was struggling with the issue of how to inform the browser that two or more media queries are applied, and although you don’t deal with that directly in your article, it gave me a great idea. Never would’ve thought of using JSON.)

    • Paul d'Aoust
      Permalink to comment#

      Another thought: is there anything wrong with using just the head { font-family: ... } hack for all browsers, rather than using both that and body::before?

    • Les James
      Permalink to comment#

      Using the head font-family trick doesn’t work in Opera. Instead of returning the declared value it returns the actual value. So no matter what you put in there Opera will return “Times New Roman”. Once Opera completely switches to Webkit it might be worth revisiting the compatibility of using font-family to pass strings.

    • What about the content property? Could this work?

  6. Usually with this kind of approach (using content to add some flag to the body) you’re limited by media queries having to be mutually exclusive.

    Here though you’ve completely sidestepped that problem by generating a JSON array so the queries can cascade. Very clever and a great approach. Nice one!

    • Paul d'Aoust
      Permalink to comment#

      I know; I had a brainstorm too when I was reading the article. I think his technique would still only allow one query to be active at a given time, but would allow you to provide a list of fallbacks. I think that, if you wanted to have two media queries active at once, you’d have to do some pretty fancy checking and have permutations for each combination of queries. Here’s the germ of an idea, since I need to be building this anyway:

      $query-and-label-list: ();
      
      @mixin breakpoint($query, $label) {
          // Check to see if this query and label have been added to the list yet.
          @if not index($query-and-label-list, ($query $label)) {
              // Build all the new permutations into a set of queries and labels to
              // output.
              $new-permutations: permute-queries($query-and-label-list, ($query $label));
              @each $new-permutation in $new-permutations {
                  $permuted-query: nth($new-permutation, 1);
                  $permuted-labels: nth($new-permutation, 2);
                  @media #{$permuted-query} {
                      body::before {
                          content: '{"current": [#{$permuted-labels}]}';
                      }
                  }
              }
              // Add the new query/label to the list, so we can build permutations
              // with it the next time a new query is added.
              $query-and-label-list: append($query-and-label-list, ($query $label));
          }
          // Now, output the styles.
          @media #{$query} {
              @content;
          }
      }
      

      Not shown is the permute-queries() function, which figures out all the different combinations of ‘label queries’ to output. Of course, as you add queries, your number of permutations will rise exponentially — you’ll have n2 permutations, to be exact. That would mean that, for three queries, you’d have nine label queries, and some of them wouldn’t make sense — @media (max-width: 320px) and (min-width: 321px) ?! We wouldn’t have to worry about confusing the browser, because that query wouldn’t apply, but it’d really make a bloaty stylesheet.

      Enter @media query parsing right in your Sass code to weed out nonsensical queries. This is starting to sound like a headache.

  7. Roman H.
    Permalink to comment#

    Seems it doesn’t work on < IE8, I‘ve tested on BrowserStack, but you always use conditional IE comments or server site detection.

    • Les James
      Permalink to comment#

      If you are testing the Codepen demo it won’t work in IE8. For a robust cross browser solution look at the JavaScript in my jQuery plugin for this.

  8. That’s cool!

    I have been thinking for a while about how to send information from CSS to JS. I experimented with a different approach were your Sass actually outputs JSON or JavaScript files as well as CSS – https://github.com/edwardoriordan/sass-attributes.

    If we can figure out ways to send data from CSS to JS (apart from parsing all of the CSS with Javascript at run time – which is probably too slow most of the time) we could start developing author friendly polyfils for CSS in JS.

  9. According to the spec (AFAIK, but correct me if I’m wrong), the following should work and would be a hell of a lot simpler if it did:

    http://codepen.io/jjenzz/pen/jKsom

    The key is the url type-or-unit argument that is passed to attr(). Maybe there’s a way to create a ployfill for this until browser support matches the spec?

  10. I built a small JavaScript object and Sass mixin to do this a while back. Would love some contributions to make it better. https://github.com/danott/breakpoints

  11. Chris
    Permalink to comment#

    This is certainly a clever solution, but wouldn’t it kill your site’s SEO as it pertains to images? Will a search crawler read the images’ dynamically-added src attribute AFTER the JS has done its thing? To my knowledge, crawlers don’t read data-* attributes.

    Perhaps a web app would be a more appropriate use-case for this where SEO isn’t as important?

    • Les James
      Permalink to comment#

      Yes, SEO is a concern with responsive image hacks. I’m not sure if bots will index images in the <noscript> fallback or not. All the more reason why we need a standard.

  12. Les James
    Permalink to comment#

    I started working on some Sass functions to make lists and stringify them into JSON. It’s only single dimensional right now but it’s a start.
    http://codepen.io/lesjames/pen/ulcFp

  13. The resize handler should be debounced since the browser fires this event continuously (every few milliseconds) during the user action. For instance, in my test, manually shrinking the browser window resulted in ~20 resize events. Since a “heavy” operation is performed inside the handler (querying all <img> elements, etc.), this operation should not be forced to execute multiple times during a single user-triggered resize action.

    See: Event debouncing

  14. Uh, I don’t think that this getComputedStyle code is a good approach. The functionality of retrieving CSS styles in a cross-browser way is already being provided by general 3rd party JS libraries (like jQuery). Manually feature detecting getComputedStyle and implementing a custom polyfill only makes this article more complex and confusing than it should be. The job of JS in this pattern is simple: 1. retrieve the current label name from body‘s CSS styles, 2. query all images, 3. loop images and set src property accordingly. This can be written in a few lines of code using a JS library. Trying to manually implement this stuff only reduces the reliability of our solution as your code is more likely to break.

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".