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 src="large.jpg" media="(min-width: 45em)" />
<source src="med.jpg" media="(min-width: 18em)" />
<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
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
- Kitty Giraudel has a library for encoding and decoding JSON from Sass.
- Robert Balicki has a library for turning JSON into Sass variables.
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.
Thanks Pedro. Using this on projects is really easy. It was a lot of work to get to this point though.
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.
That’s an impressive technique. Thanks for sharing.
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
Am I missing something or browsers that don’t support generated content also don’t support Media Queries? (I’m talking about IE8).
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 thatlt-ie9
class.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.)
Another thought: is there anything wrong with using just the
head { font-family: ... }
hack for all browsers, rather than using both that andbody::before
?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?
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!
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:
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.Seems it doesn’t work on < IE8, I‘ve tested on BrowserStack, but you always use conditional IE comments or server site detection.
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.
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.
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 toattr()
. Maybe there’s a way to create a ployfill for this until browser support matches the spec?Sorry, the links were all broken…
1) http://codepen.io/jjenzz/pen/jKsom
2) http://www.w3.org/TR/css3-values/#attr-notation
When I read this post by Christian Heilmann I thought of trying the same thing. It seems like a JavaScript free solution is in there somewhere.
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
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?
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.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
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
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 detectinggetComputedStyle
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 frombody
‘s CSS styles, 2. query all images, 3. loop images and setsrc
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.