A dancer balancing on one hand A dancer jumping on a bridge A dancer in front of a black-and-white background A dancer in the center of a bridge

Some time ago I wrote a small script that automated the process of creating a responsive image carousel. While the same result could be achieved using pure CSS, doing so required that keyframes had to be recalculated when the number of images changed. Written in pure , my solution retained the performance advantages of by writing a keyframe animation using a wide range of options.

The script presented here accomplishes a similar result, only with a fade-in sequence: again, you can add as many images as you like, in the order you wish. The only requirement is that that all the images must be all the same size:

<figure id="fadey">
    <img src="dancer-arch.jpg" alt>
    <img src="white-bridge-jump.jpg" alt>
    <img src="dancer-beams.jpg" alt>
    <img src="bridge-white-walker.jpg" alt>
</figure>

I’d advise using srcset, the w descriptor and the sizes attribute for each image to achieve the best results:

<img src="dancer-arch.jpg"
srcset="dancer-arch.jpg 750w, dancer-arch-2x.jpg 1500w" 
sizes="(min-width: 750px) 750px, 100vw">

There’s also some basic CSS that is used to format the images and gallery container:

#fadey { 
    width: 100%; 
    position: relative;
}
#fadey img {
    display: block;
    width: 100%;
}

The script is added at the end of the page:

var cssFadey = function(newOptions) {
    var options = (function() {
        var mergedOptions = {},
        defaultOptions = {
            presentationTime: 3,
            durationTime: 2,
            fadeySelector: '#fadey',
            cssAnimationName: 'fadey',
            fallbackFunction: function() {}
        };
        for (var option in defaultOptions) mergedOptions[option] = defaultOptions[option];
        for (var option in newOptions) mergedOptions[option] = newOptions[option];
        return mergedOptions;
    })(),
    CS = this;
    CS.animationString = 'animation';
    CS.hasAnimation = false;
    CS.keyframeprefix = '';
    CS.domPrefixes = 'Webkit Moz O Khtml'.split(' ');
    CS.pfx = '';
    CS.element = document.getElementById(options.fadeySelector.replace('#', ''));
    CS.init = (function() {
        if (CS.element.style.animationName !== undefined) CS.hasAnimation = true;
        if (CS.hasAnimation === false) {
            for (var i = 0; i < CS.domPrefixes.length; i++) {
                if (CS.element.style[CS.domPrefixes[i] + 'AnimationName'] !== undefined) {
                    CS.pfx = domPrefixes[i];
                    CS.animationString = pfx + 'Animation';
                    CS.keyframeprefix = '-' + pfx.toLowerCase() + '-';
                    CS.hasAnimation = true;
                    break;
                }
            }
        }
        if (CS.hasAnimation === true) {
			function loaded() { 
				var imgAspectRatio = firstImage.naturalHeight / (firstImage.naturalWidth / 100);   
				var imageCount = CS.element.getElementsByTagName("img").length,
				totalTime = (options.presentationTime + options.durationTime) * imageCount, 
				css = document.createElement("style"); 
				css.type = "text/css";
	            css.id = options.fadeySelector.replace('#', '') + "-css";
	            css.innerHTML += "@" + keyframeprefix + "keyframes " + options.cssAnimationName + " {\n";
	            css.innerHTML += "0% { opacity: 1; }\n";
	            css.innerHTML += (options.presentationTime / totalTime) * 100+"% { opacity: 1; }\n";
	            css.innerHTML += (1/imageCount)*100+"% { opacity: 0; }\n";
	            css.innerHTML += (100-(options.durationTime/totalTime*100))+"% { opacity: 0; }\n";
	            css.innerHTML += "100% { opacity: 1; }\n";
  css.innerHTML += "}\n";
	            css.innerHTML += options.fadeySelector + " img { position: absolute; top: 0; left: 0; " + keyframeprefix + "animation: " + options.cssAnimationName + " " + totalTime + "s ease-in-out infinite; }\n";
	          css.innerHTML += options.fadeySelector + "{ box-sizing: border-box; padding-bottom: " + imgAspectRatio + "%; }\n";
	          for (var i=0; i < imageCount; i++) {
	            css.innerHTML += options.fadeySelector + " img:nth-last-child("+(i+1)+") { " + keyframeprefix + "animation-delay: "+ i * (options.durationTime + options.presentationTime) + "s; }\n";
	            }
			document.body.appendChild(css); 
		}
		var firstImage = CS.element.getElementsByTagName("img")[0];
		if (firstImage.complete) {
			loaded();
		} else {
			firstImage.addEventListener('load', loaded);
			firstImage.addEventListener('error', function() {
				alert('error');
			})
		}
	} else {
		// fallback function
		options.fallbackFunction();
        }
    })();
}

The script is called with:

cssFadey();

There are several possible options when you call the function, which I’ll cover in a moment. A quick explanation of the defaults:

  • presentationTime is the amount time each image appears on screen, measured in seconds (three seconds, by default)
  • transitionTime is the time of the fade between each image, also measured in seconds (two seconds, by default)
  • fadeySelector is the id of the element that contains the images (by default this is a <figure> element with an id of fadey, although the element and id used is up to you
  • cssAnimationName is the name of the keyframe animation generated by the script
  • fallbackFunction is the JavaScript function you wish to use in case the browser does not support CSS animation
  • the script checks if the browser needs a vendor prefix for CSS animation (this is increasingly rare in modern browsers, thankfully)

The script creates an animation for each of the images and adds it in a stylesheet at the bottom of the page. An animation for four images with a duration of four seconds each, with a transition of two seconds, would generate the following:

@keyframes fadey {
    0% { opacity: 1; }
    16.66% { opacity: 1; }
    25% { opacity: 0; }
    91.66% { opacity: 0; }
    100% { opacity: 1; }
}

Each image in the container is directed to the same animation:

#fadey img {
    position: absolute; top: 0;
    animation: fadey 24s ease-in-out infinite;
}

The container is sized with padding-top as percentage value, since the absolute positioning of the images will not contribute to the height of the container. The percentage amount is determined by finding the first loaded image in the container and dividing its width by its height:

imgAspectRatio = firstImage.naturalHeight / (firstImage.naturalWidth / 100)

The total duration of the animation is equal to the total number of images × (presentationTime + transitionTime)

Each of the images is provided with an animation-delay. The generated CSS counts backwards from the last image in the container element (one that, due to the absolute positioning, is at the top of the image stack) using nth-last-child. CSS produced for the example above would be:

#fadey img:nth-last-child(1) { animation-delay: 0s; }
#fadey img:nth-last-child(2) { animation-delay: 6s; }
#fadey img:nth-last-child(3) { animation-delay: 12s; }
#fadey img:nth-last-child(4) { animation-delay: 18s; }

If you want to call the script using custom options on this markup:

<figure id="gallery">
    …
</figure>

Use the following:

cssFadey({
    presentationTime: 3,
    durationTime: 2,
    fadeySelector: '#gallery',
    cssAnimationName: 'sequence'
});

I’ve also created a CodePen repo for comments and contributions.

The one downside of this technique is that there’s no immediate method of jumping manually from one image to another. To keep the efficiency of CSS animation, manual control will mean the use of Web Animation API, as I’ll show in the next article.

Photographs by Jiachuan Liu, Daniel Girizd, licensed under Creative Commons

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/GoMaBL