Photograph of a woman's face under a Bedouin headress
Bedouin
Photograph of a man's blue-green-eyes
Blue-green
Photograph of dramatic fake eyelashes in closeup
Dramatic Fake
Photograph of a girl in heavy snow
Snow

Before the iPhone there was the Rolodex, a wheeled index card contact management system popular from the late 1950’s to the mid-80s. Recently, it occurred to me that the device’s UI might be brought into the 21st century and applied to a CSS 3D image gallery.

RolodexInstead of a wheel, I decided to use HTML5 range input to flick the images forward and back. JavaScript is used to link the input actions to the orientation of the images. I’ve made the CSS and scripting as adaptive as possible: in theory, an ImgDex could consist of four images or twenty, with no code change required.

The content of the page is a series of images that are all the same size inside <figure> elements with separate <figcaption> descriptions:

<div id="imgdex">
	<figure>
		<img src="arabic-eyes.jpg" alt="Bedouin headress">
		<figcaption>Bedouin</figcaption>
	</figure>
	<figure>
		<img src="blue-green-eyes.jpg" alt="Blue-green-eyes">
		<figcaption>Blue-green</figcaption>
	</figure>
	<figure>
		<img src="fake-eyelashes.jpg" alt="Fake eyelashes">
		<figcaption>Dramatic Fake</figcaption>
	</figure>
	<figure>
		<img src="snow-queen.jpg" alt="A girl in heavy snow">
		<figcaption>Snow</figcaption>
	</figure>
</div>

The CSS (sans vendor prefixes) is relatively straightforward:

#imgdex {
	position: relative;
	perspective: 4000px;
	transform-style: preserve-3d;
	padding-top: 58%; 
}
#imgdex figure, #imgdex figure figcaption { 
	position: absolute;
	transition: 1s ease-in-out;
}
#imgdex figure {
	top: 0; left: 120px;
	transform-origin: left bottom;
	width: 70%;
}
#imgdex figure figcaption {
	bottom: 0;
	font-size: 1.2rem;
	left: -8rem;
	opacity: 0;
}
#imgdex figure:last-of-type {
	transform: rotateX(5deg);
	box-shadow: 0px 0px 200px rgba(0,0,0,0.5);
}

The <div> sets up the perspective plane for the manipulation of the images. Every <figure> element except the last will be rotated forward from their lower edges to be almost “flat” with JavaScript. The very last element – the one at the “back” of the stack – is brought upright, leaned back a little, and provided with a box-shadow. This last <figure> won’t move, but the others will.

Below the <div>, a range input:

<input type="range" min="1" onfocus="this.oldvalue = this.value;" oninput="updateImage(this);this.oldvalue = this.value;" id="ranger">

When the slider is moved, the element calls on the updateImage() function and passes its current value and previous value.

But before the slider can be used we need to provide it with an upper limit, corresponding to the total number of <figure> elements, and set the default value to that same number. We’ll place the script to do so at the bottom of the page:

var imgdex = document.getElementById('imgdex'),
figs = imgdex.querySelectorAll('figure'),
imgcount = figs.length;
ranger.max = imgcount;
ranger.value = imgcount;

Finally, we need to move the figures based on the position of the slider. Continuing the script:

for(var i=0;i<(imgcount-1);i++) {
	var rotation = parseFloat(-92 + "." + (imgcount - i));
	figs[i].style.transform = 'rotateX(' + rotation + 'deg)';
}
document.querySelector('#imgdex figure:last-child figcaption').style.opacity = 1;

function updateImage(slider) {
	var currentimg = document.querySelector('#imgdex figure:nth-child('+slider.value+')');
	if (slider.oldvalue !== undefined) {
		var oldimg = document.querySelector('#imgdex figure:nth-child('+(slider.oldvalue)+')');
	} else {
		slider.oldvalue = imgcount;
		var oldimg = document.querySelector('#imgdex figure:nth-child('+(slider.oldvalue)+')');
	}
	if (slider.value < slider.oldvalue) { 
		currentimg.style.transform = 'rotateX('+slider.value+'deg)';
	} 
	if (slider.value > slider.oldvalue) {
		var rotation = parseFloat(-92 + "." + (imgcount - slider.value));
		oldimg.style.transform = 'rotateX(' + rotation + 'deg)';
   }
   if (slider.value !== slider.oldvalue) {
	   currentimg.querySelector('figcaption').style.opacity = 1;
	   oldimg.querySelector('figcaption').style.opacity = 0;
   }
}

This code is a little more complex:

  1. First, it rotates all but the very first <:figure> element forward. The amount of rotation decreases slightly for each, so that the images are not perfectly coplanar: otherwise, weird flickering and image mashing occurs during animation.
  2. Next, the current value of the slider - and thus the current image - is determined, and oriented to the correct angle with its caption made visible.

There are many possible improvements to the code, but this should be enough to get anyone interested started on making their own version.

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/LELLaZ