Better Responsive Images Using Multiple Backgrounds
.bg-image {
width: 100%;
/* default background-position */
background-position: center center;
/* lt ie 8 */
-ms-background-position-x: center;
-ms-background-position-y: center;
/* scale proportionately */
background-size: cover;
/* IE8 workaround - http://louisremi.github.io/background-size-polyfill/ */
-ms-behavior: url(/backgroundsize.min.htc); }
/* mobile-first default (b&w) */
.bg-image-sedona {
background-image: url(img/photo-sedona_512x320.jpg);
background-position: center 21%; }
/* example media queries (IE8 needs this:
http://code.google.com/p/css3-mediaqueries-js) */
@media
/* "mama-bear" - plus any-retina */
only screen and min-width : 513px,
only screen and (-webkit-min-device-pixel-ratio: 1.5),
only screen and ( min-device-pixel-ratio: 1.5) {
/* mid-size (sepia) */
.bg-image-sedona {
background-image: url(img/photo-sedona_1024x640.jpg); }
}
@media
/* "papa-bear" - plus larger retina */
only screen and (min-width : 1025px),
only screen and (min-device-width : 768px) and (-webkit-min-device-pixel-ratio: 1.5),
only screen and (min-device-width : 768px) and ( min-device-pixel-ratio: 1.5) {
/* high-res (color) */
.bg-image-sedona {
background-image: url(img/[email protected]); }
}
The div displaying the background-image needs a height, which can be set manually, or, as I’ve done here, by wrapping a transparent “proxy” img
set to scale responsively (more on this here).
Now, as you may have noticed, the first time a page renders a large image, there can be a noticeable delay as it loads. Even smaller images, before they’re cached, may display an annoying flash as they load or get swapped in. But we can fix that…
CSS3 multiple-backgrounds: How They Stack Up
Newer browsers let us stack background images by declaring multiple values separated by a comma. In this way, we can display the original cached image while the replacement image smoothly loads over it (note the stacking order in the code below).
To see this at work, narrow the browser window and empty the cache (choose “Clear Browsing Data” in the Chrome menu or “Empty Caches” in Safari’s Develop menu). Now reload the page. Scroll back down here and widen the window until the color images load above. (Or try this pop-up window.)
Unfortunately, older browsers such as IE8* see multiple background declarations and throw up their hands — displaying nothing (yikes!). So we need to use modernizr.js to feature-detect, and create a fallback (if we want those browsers to show something larger than the mobile-first default):
/* .bg-image and .bg-image-sedona same as above.
.multiplebgs class added by modernizer.js. */
@media
/* "mama-bear" - plus any-retina */
only screen and min-width : 513px,
only screen and (-webkit-min-device-pixel-ratio: 1.5),
only screen and ( min-device-pixel-ratio: 1.5) {
/* no-multiplebgs - mid-size fallback (sepia) */
.no-multiplebgs .bg-image-sedona,
/* upscale to mid-size if no javascript */
.no-js .bg-image-sedona {
background-image: url(img/photo-sedona_1024x640.jpg); }
.multiplebgs .bg-image-sedona {
background-image:
/* mid-size on top (sepia) */
url(img/photo-sedona_1024x640.jpg),
/* mobile-first default on bottom (b&w) */
url(img/photo-sedona_512x320.jpg);
}
}
@media
/* "papa-bear" - all three images */
only screen and (min-width : 1025px) {
/* no-multiplebgs fallback is above */
.multiplebgs .bg-image-sedona {
background-image:
/* high-res on top (color) */
url(img/[email protected]),
/* mid-size in middle (sepia) */
url(img/photo-sedona_1024x640.jpg),
/* mobile-first default on bottom (b&w) */
url(img/photo-sedona_512x320.jpg);
}
}
@media
/* larger retina device - triggered immediately,
so mid-size image not needed */
only screen and (min-device-width : 768px) and
(-webkit-min-device-pixel-ratio: 1.5),
only screen and (min-device-width : 768px) and
( min-device-pixel-ratio: 1.5) {
/* no-multiplebgs fallback is above */
.multiplebgs .bg-image-sedona {
background-image:
/* high-res on top (color) */
url(img/[email protected]),
/* mobile-first default on bottom (b&w) */
url(img/photo-sedona_512x640.jpg);
}
}
Standard vs. Progressive JPEGs
For JPEGs, the way an image renders over another image in a multiple background depends on how it’s been saved. A standard JPEG “paints” the image sequentially as it’s downloading. Progressive JPEGs “pop on” once completely downloaded. (The standard way seems smoother to me.) Note that image compressors like ImageOptim have their defaults set to save progressively (Jpegrescan is checked) because it saves a little space.
Of course, we don’t want users to download images unnecessarily, or overcomplicate our upkeep, so it’s important we keep our breakpoints restrained and think them through logically. But now that we can make image swapping less conspicuous, it opens up some possibilities…
Faking “lowsrc”
Back in the days when steam powered the Internet, dial-up access was so slow they created a special attribute so that users would see something during the minute and a half it took to download their animated gifs: it was called “lowsrc” and it looked like this:IMG SRC="big.gif" LOWSRC="small.gif"
.
Browsers stopped supporting this back in the late ’50s.
But something like this might be handy now, so that users can see something during the two-and-a-half seconds it takes to download their retina-ready high-res images. (And don’t forget, 4K is coming.)
Modern browsers are pretty smart about filling in images as soon as they’re fetched, so by specifying smaller, more compressed “lowsrc” images as the default, then including them stacked beneath the @2x retina images in our CSS media queries, things are likely to feel snappier. We can go one step further using jQuery…
The idea is to hold off image swapping until the page is rendered completely with our default “lowsrc” images. Then we use jQuery to add an “hd” class to our main “bg-image” class, which triggers our media queries to swap the images. We could also hold off and “lazy load” the higher-res images as we scroll to them, using something like the jQuery Waypoints plug-in.
/* .bg-image and .bg-image-sedona same as above
.hd class added by jQuery after page loads
(or perhaps "lazy loaded" as user scrolls) */
@media
/* "mama-bear" - plus any-retina */
only screen and (min-width : 513px),
only screen and (-webkit-min-device-pixel-ratio: 1.5),
only screen and ( min-device-pixel-ratio: 1.5) {
/* no-multiplebgs - mid-size fallback */
.no-multiplebgs .bg-image-sedona.hd,
.no-js .bg-image-sedona {
/* mid-size (sepia) */
background-image: url(img/photo-sedona_1024x640.jpg); }
.multiplebgs .bg-image-sedona.hd {
background-image:
/* mid-size on top (sepia) */
url(img/photo-sedona_1024x640.jpg),
/* mobile-first "lowsrc" on bottom (b&w) */
url(img/photo-sedona_512x320.jpg); }
}
@media
/* "papa-bear" - size only */
only screen and (min-width : 1025px) {
/* no-multiplebgs fallback is above */
.multiplebgs .bg-image-sedona.hd {
background-image:
/* high-res on top (color) */
url(img/[email protected]),
/* mid-size in middle (sepia) */
url(img/photo-sedona_1024x640.jpg),
/* mobile-first "lowsrc" on bottom (b&w) */
url(img/photo-sedona_512x320.jpg); }
}
@media
/* larger retina device, triggered immediately,
so mid-size image is not needed */
only screen and (min-device-width : 768px) and
(-webkit-min-device-pixel-ratio: 1.5),
only screen and (min-device-width : 768px) and
( min-device-pixel-ratio: 1.5) {
/* no-multiplebgs fallback is above */
.multiplebgs .bg-image-sedona.hd {
background-image:
/* high-res on top (color) */
url(img/[email protected]),
/* mobile-first "lowsrc" on bottom (b&w) */
url(img/photo-sedona_512x320.jpg); }
}
/* waits until everything is loaded, not just DOM is ready */
$(window).load(function() {
$('.bg-image').addClass('hd');
});
See this “Faking lowsrc” demo in action
See an example with “lazy loading” at work.
/* "lazy loads" when .bg-image appears in viewport -
http://imakewebthings.com/jquery-waypoints/ */
$('.bg-image').waypoint(function(direction) {
if (direction === 'down') {
$(this).addClass('hd');
}
}, { offset: 'bottom-in-view', triggerOnce: true });
/* other offsets: '100%' (image top at viewport bottom),
'125%' (just beyond the viewport, about to scroll in) */
Wrapping Up
Ideally, I’d like to see this work in a more automated way, like picturefill.js, but extrapolating from a mobile-first img
rather than a data-src
attribute. What do you think? You can take a look at the source code for more, see all the demos on CodePen, ordownload the example files here. If you have any questions, comments, or corrections, drop me a line: [email protected].