The Layered Look: Better Responsive Images Using Multiple Backgrounds

Guest Author //

The following is a guest post by Parker Bennett. Parker has written for CSS-Tricks before, in his article Crop Top dealing with the positioning of fluid images. This is a great follow up to that, presenting another option in the never-ending responsive images saga. It's also an interesting contrast to yesterday's post on responsive images - so you can see how very different the approaches to this problem can be.

We want fast page loads. We want retina images. Can we have it all?

Faking "lowsrc" — high-res color swaps in once mobile-first is loaded.

We don't want to serve giant-sized images to people who don't need them. There are several approaches for not doing this — including Scott Jehl's picturefill, server-side solutions, and lazy-loading techniques — but the simplest method may be to use a background-image and the magic of CSS media queries. This way, your lucky retina users get the high-res @2x versions, and the rest of us… well, at least I don't have to wait for mammoth files to download.

(To help illustrate below, I've made my "mobile first" default image black and white, the medium-sized version sepia, and there's a larger color version if you resize wide enough or are on a high-dpi screen.)

Small black & white default, swaps to color when wider. Before images are cached there's a small flash while they load.
/* base background-image class */
.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/photo-sedona_1024x640@2x.jpg); }
  }

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

Edit on CodePen

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).

Single background on top, multiple backgrounds on bottom.

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/photo-sedona_1024x640@2x.jpg),
        /* 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/photo-sedona_1024x640@2x.jpg),
        /* 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"

Edit on CodePen

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/photo-sedona_1024x640@2x.jpg),
        /* 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/photo-sedona_1024x640@2x.jpg),
        /* 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, or download the example files here. If you have any questions, comments, or corrections, drop me a line: parker@parkerbennett.com.


* IE8 doesn't support multiple backgrounds, but if you can declare a width and height for your image, you could work something similar using this pseudo-element approach by Nicolas Gallagher.