How to make responsive images using CSS and JSON
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 medi
um
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.