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 placeholder, 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 also require a costly reflow on image load without fixed dimensions.

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:16x9">
  <div data-src="" data-alt=""></div>
</div>

This technique requires a wrapper as well as a placeholder. The inner width of the wrapping block will be used to calculate the right size space in the layout. Attributes to be transferred to the eventual image element are specified as data attributes on the placeholder element.

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 wrapper width. */
.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 space in the layout. My code relies on knowing the aspect ratio of the target image which I think is fine in the majority of cases, but it could also be calculated on the server for each image and output as an inline style of the placeholder element.

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);
    }
}

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

.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;
}

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

View the demo on GitHub

Comments