Responsive image placeholders

Lazy and conditional image loading is a pretty common design pattern and more or less a necessity for larger responsive sites. The most common implementations swap a placeholder for the loaded image, either by using an element as a marker, E.G. <div data-image="*"> or an image tag with a temporary source. The problem is that neither of these are particularly well suited to a flexible layout, both techniques will require a costly reflow on image load.

View the demo on GitHub

Using a little basic CSS, JavaScript and a dash of animation it’s simple to avoid the performance hit of re-calculating layout and provide a smooth user experience.

The markup

<div class="defer-image image-ratio:16:9">
  <div data-src="" data-alt=""></div>
</div>

This technique requires a wrapper as well as a marker. The inner width of the wrapping block will be used to create the placeholder and any layout should be applied to it, unless the image is intended to sit as a full-width block. Attributes to be transferred to the replacement image element are specified as data attributes on the marker.

The CSS

.no-js .defer-image {
    display: none;
}

.defer-image > img {
    display: block;
    min-width: 100%;
    max-width: 100%;
}

.defer-image.is-loading {
    position: relative;
    background: #EEE;
}

/* Image aspect ratios - % is relative to width. */
.image-ratio\:1x2   > div { padding-top: 200%; }
.image-ratio\:9x16  > div { padding-top: 177.777%; }
.image-ratio\:2x3   > div { padding-top: 150%; }
.image-ratio\:3x4   > div { padding-top: 133.333%; }
.image-ratio\:1x1   > div { padding-top: 100%; }
.image-ratio\:4x3   > div { padding-top: 75%; }
.image-ratio\:3x2   > div { padding-top: 66.66%; }
.image-ratio\:16x9  > div { padding-top: 56.25%; }
.image-ratio\:2x1   > div { padding-top: 50%; }

CSS margin and padding percentage values are calculated based on the width of the containing block and this effect is used to create the placeholder itself. The technique relies on knowing the aspect ratio of the target image which I think is fine in the majority of cases.

The JavaScript

var deferImage = function(element) {
    var i, len, attr;
    var img = new Image();
    var placehold = element.children[0];

    element.className+= ' is-loading';

    img.onload = function() {
        element.className = element.className.replace('is-loading', 'is-loaded');
        element.replaceChild(img, placehold);
    };

    for (i = 0, len = placehold.attributes.length; i < len; i++) {
        attr = placehold.attributes[i];
        if (attr.name.match(/^data-/)) {
            img.setAttribute(attr.name.replace('data-', ''), attr.value);
        }
    }
}

No monolithic library is required, the core of this technique is just plain old (in both senses) JS. In future it could be updated to use the ES5 array .forEach() method, classList and dataset APIs but they offer no immediately useful benefits here other than providing nice, terse syntax. The script adds classes to imply state and act as hooks for styling. Importantly, the onload event listener is applied before the src attribute to avoid it not being triggered in legacy Internet Explorer in the case that an image is loaded from the cache.

The animation

@keyframes bobble {
    0% {
        opacity: 0;
        transform: translateY(0);
    }
    35% {
        opacity: 1;
        transform: translateY(-20px);
    }
    100% {
        opacity: 0;
        transform: translateY(0);
    }
}

.defer-image.is-loading::after {
    content: ' ';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 1em;
    height: 1em;
    margin: -0.5em 0 0 -0.5em;
    background: rgba(125, 125, 125, 0.5);
    border-radius: 100%;
    animation: bobble 2s cubic-bezier(0.6, 1, 1, 1) infinite;
}

@keyframes fadeIn {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

.defer-image.is-loaded > img {
    animation: fadeIn 1s both;
}

No one wants to see preloaders on the web now but it’s preferable to infer something is loading than to leave a rectangular gap, especially on flaky connections where multiple requests can cause issues. There’s no loading .gif files here though, instead leveraging pseudo-elements and keyframe animations to create a little loading bobble that will be smooth and resolution independent.

Conditionally loading images is a key performance technique, whether it’s for loading images of a different resolution or deciding if a browser cuts the mustard. The extra performance of minimising layout re-calculation can make an appreciable difference.

The source code and demo is available on GitHub.

comments powered by Disqus

About the Author

A photo of Matt Hinchliffe

Matt Hinchliffe

I'm a 26 year old UI developer working at Lonely Planet based in London. I specialise in crafting scalable, performance-driven code, tackle accessibility issues and keep an opinionated interest in the latest hotness. I like my tea robustly brewed, white and with no sugar, thanks!