Responsive Photosets

Creating beautiful photosets with jQuery and CSS

Terry Mun
Coding & Design

--

Erratum: This writing has been updated as of 16th December 2013, for a minor calculation mistake that led to distortion of aspect ratio when margins are accounted for.

As I was developing functionality for a new personal blog, I ran into an issue — being an avid amateur photographer myself, I would love to display images in a photoset. One of the charms of Tumblr is that the photosets allow you to display multiple images in a certain layout, and I have always wanted to replicate this, although with a slightly different implementation. As a person who have dipped his feet in web design, I am also acutely aware how important fluid or responsive layouts is — so I sought to incorporate this very concept into a flexible photoset.

The requirements

There are some simple requirements to the aforementioned photoset:

  1. Flexibility, such that it fits into a fluid width layout
  2. Customizability, such that one can dictate the layout as desired, preferably in a straight forward, fuss-free manner — and such that it can also be adapted into a WordPress theme function
  3. Equal height images, such that all images fit snugly into a single row, but their widths are adjusted accordingly so that all images on the same row fill the entire width, not more and not less, of its parent container, while preserving their individual aspect ratios.

Now, where do we begin? I searched high and low, but there doesn’t seem to be a satisfactory solution anywhere. While WordPress offers a tiled mosaic display (by enhancing the native [gallery] shortcode) with their JetPack plugin, I did not want all the bells and whistles of the plugin, which bloats the site. Also, I did not need images to span rows (which requires more complicated code work), so the idea of installing JetPack seemed even less tantalizing.

In the end, it seems like I would have to write my own jQuery function — and that screams challenge. The first two requirements are easy to satisfy, but the last one left me in a tizzy… until I had an epiphany on a very boring Tuesday night.

Here is the actual demo, hosted with CodePen, of the technique I will introduce to you in this article.

HTML markup

The HTML markup is rather straight forward:

<div class="photoset">
<div class="photoset-row">
<figure class="photoset-item">
<a href=""><img src="" alt="" title="" /></a>
<figcaption></figcaption>
</figure>
<!-- multiple images per row is possible -->
</div>
<!-- Additional rows are possible -->
<div class="photoset-row">
<!-- more photoset items -->
</div>
</div>

To those who are doubtful of nesting other block-level elements within the anchor element, this is actually possible (and semantically valid) in HTML5 — provided that there are no interactive content in it. Just remember to declare the right doctype at the start of your file.

The <a> element allows you to link to larger versions of the thumbnail image, or even trigger modal boxes. I will leave these functionality out not only for the sake of brevity, but also because there are not necessary for the core functionality of the photoset.

Styling is rather straight-forward, and I highly recommend using a CSS reset. What is achieve in the CSS code block below can be summarized as follow:

  • Creating general styles for each row,
  • Floating individual items in each row, and
  • Styling the image caption, so that it appears upon hover. This exploits the CSS translate property.
.photoset {
overflow: hidden;
width: 100%;
}
/* Rows */
.photoset .photoset-row {
margin-bottom: .5rem;
overflow: hidden;
width: 150%; /* See comment after code for reason */
}
.photoset .photoset-row:last-child { margin: 0; }
/* Images on each row, known as an "item" */
.photoset .photoset-item {
display: block;
float: left;
margin: 0 .25rem;
}
.photoset .photoset-item:first-child { margin-left: 0; }
.photoset .photoset-item:last-child { margin-right: 0; }
/* Images and captions are contained within <figure> */
.photoset figure {
margin: 0;
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
}
.photoset figcaption {
background-color: rgba(255, 255, 255, .75);
box-sizing: border-box;
font-size: .75rem;
padding: .5rem;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
-webkit-transform: translateY(100%);
transform: translateY(100%);
transition: all .5s ease-in-out;
}
.photoset a:hover figcaption {
-webkit-transform: translateY(0);
transform: translateY(0);
}
.photoset img {
display: block;
max-width: 100%;
transition: all .125s ease-in-out;
}

You may ask why I have chosen to set the width of each row in the photoset to 150% instead of 100%. There is no risk of images running out of the stipulated width, since their final widths will be calculated with respect to the width of the parent container, the photoset itself.

Width exceeding 100% prevents floats from wrapping

By declaring a width exceeding 100%, yet not too taxing on the rendering system (since the browser actually does render a box of said specified width — this is why the -9999px text-indent was frowned upon), we can ensure that floats will not wrap when the browser window is being resized. The additional width beyond 100% allows floats to stay on a single line while our JavaScript function is busy with recalculating the correct image dimensions. I have picked 150% as a comfortable compromise, but you can definitely explore larger percentage values.

jQuery coming to the rescue

Now the fun part begins — making jQuery to perform the third requirement, which is to calculate the widths of each individual image in each row such that they are have equal height, and their combined widths, taking into account the horizontal margins between each images, fills the entire width of the photoset container. Challenging? Definitely. Impossible? I don’t take no for an answer.

Mathematical basis

Before I dump a whole chunk of code on the monitor, I would like to introduce you a walk-through of the mathematical basis behind the calculations — at least for the initiated. This is because it’s quite impossible to grasp the calculations performed later unless I explain them right now.

There is only one variable to be computed.
The rest are relative.

The only variable that has to be determined is the width of one single element. Since we know that the widths are related to a to-be-calculated uniform height by known aspect ratios, they are not exactly hard to calculate, either.

Let’s start with a very simple example: we have two images on a row. One is a square (aspect ratio: 1) and the other in a landscape orientation (aspect ratio: 2). Here is an animation describing how the widths, based on a single variable, can be computed. For the sake of simplicity, we let x denote the width of the image with the smallest aspect ratio.

Simple example of two images. The total width can be easily calculated with known aspect ratios.

And since we know the width of the container, it simple means that x is one-third the width of the container. Let’s say the container is 960px wide, that means the orange box will have a dimension of 320*320, while the green box will have one of 640*320. Not rocket science, right?

I hope that was clear. Now let’s move on to a slightly more complicated example, involving aspect ratios that are fractions. Then again, for ease of calculation and programming — since we can rely on the Math.min.apply() function — we again let x denote the smallest aspect ratio. The basic principles from the previous scenario apply:

A slightly more complicated scenario involving fractional aspect ratios.

This calculation is done strictly within the context of a row, because the uniform height only applies to images on the same row. A new calculation has to be made per row, for which we will use jQuery’s .each() function.

We can see from the above animated examples that the width of the narrowest image (the one with the smallest aspect ratio) can be expressed as a function of the overall width. Since we already know the overall width, we can easily solve for x. This mathematical basis therefore forms the code that we are using later.

This is of course, a simplified scenario. In the real life scenario where we want spacing to be between images, we establish horizontal margins for all image elements, and then remove the right margin for the first image (that is on the left) and the left margin for the last image (that is on the right). However, we let CSS handle these calculations, and provide concessions in our jQuery calculations later to account for there margins.

Wait for DOM to get ready

This is really important — we want to make sure that we bind events to elements that exist at runtime, so we wait for DOM to be ready. Remember, beauty takes time!

The first thing we want to do is to hide the images by forcing zero dimension on both axes, wait for them to load, and then run the calculations when the dimensions are finally available.

// Functionality for calculations
// and event binding can be done on DOM ready
$(function (){
// Rest of the code goes here
});

That should do it. Now we wait for the browser to be done loading the images — thanks Mathew for pointing out that the layout breaks upon initial load with my initial strategy of putting all my code in the DOM ready event (instead of the load event).

Having trouble getting it to load up proper each time, though: refreshes seem to randomly break it?

The reason for waiting a for the load event is because the image dimensions will not be accessible until they have been loaded by the browser.

// Trigger the actual calculations when resources are loaded
$(window).load(function (){
// Trigger resize event to perform calculations
$(window).resize();
});

Store original image dimensions in data objects

The original image dimensions are accessed with the native naturalWidth and naturalHeight properties after the image has been loaded — we then store them in jQuery data objects. They are supported in all modern browsers (that means excluding IE8 and below). Since the images are loaded, we can also safely display them by setting opacity to 1.

// Store original image dimensions
$('.photoset-item img').each(function () {
$(this).load(function (){
$(this)
.data('org-width', $(this)[0].naturalWidth)
.data('org-height', $(this)[0].naturalHeight)
.css({ opacity: 1 });
});
});

In order to support older browsers, you may access original image dimensions by creating a new image object for every <img> element encountered, and then fetching their actual width and height respectively:

 // Store original image dimensions
$('.photoset-item img').each(function () {
var img = new Image();
$(this)
.data('org-width', img.width)
.data('org-height', img.height)
});

The above code will create a new image object per <img> element encountered, and then fetching the natural dimensions of it.

Listen to resize event

When using jQuery to calculate dimensions, one has to remember to listen to the resize event triggered on the window element.

$(window).resize(function (){
// Perform calculation for each row independently
$('.photoset-row').each(function() {
// Rest of the code here
});
});

Even better: throttle or debounce the resize event, so it does not fire a gazillion times when you (or your very curious and amused visitor) resizes the viewport. Paul Irish wrote an amazingly nifty script for that purpose. Alternatively, you can rely on the jQuery throttle/debounce plugin. For performance’s sake, I have elected to use Paul’s script in my CodePen demo.

The rest of the code below goes into the .each() function.

First of all, we make things easy by shortening variables and fetching the parent container’s width:

// Declare some variables
var $pi = $(this).find('.photoset-item'),
cWidth = $(this).parent('.photoset').width();

And then we generate an array containing all the aspect ratios of images in the same row. This can be done by using jQuery’s very handy .map() function, where we iterate through all elements that match the selector. And since we have already created the jQuery data objects storing the original image dimensions, it is just a matter of fetching them and performing a simple arithmetic operation to get the aspect ratio:

// Generate array
var ratios = $pi.map(function() {
var orgWidth = $(this).find('img').data('org-width'),
orgHeight = $(this).find('img').data('org-height');
return orgWidth/orgHeight;
}).get();

Now, we want to fetch the sum of the ratio of widths of all images in that row. This is easily done with a for loop:

// Sum aspect ratios
var sumRatios = 0,
minRatio = Math.min.apply(Math, ratios);
for (var i=0; i<$pi.length; i++){
sumRatios += ratios[i]/minRatio;
}

Also, since we want to make concessions for margins, we sum all horizontal margins of all items in the same row:

// Sum all horizontal margins
var sumMargins = 0;
$pi.each(function (){
sumMargins += parseInt($(this).css(‘margin-left’)) + parseInt($(this).css(‘margin-right’));
});

Finally, we solve for x, which denotes the width of the narrowest image in any photoset row. x is simply derived by dividing the parent container width with the sum of the aspect ratios.

This allows us to calculate the dimensions for each individual photoset item. Given the non-trivial scenario where margins are involved, we will have to subtract the sum of horizontal margins of all child items from the parent width, so that the outer width of the item (CSS width + margins) will be the desired width.

// Calculate dimensions
$pi.each(function (i){
var minWidth = (cWidth-sumMargins)/sumRatios;
$(this).find('img')
.width(Math.floor(minWidth * (ratios[i] / (Math.min.apply(Math, ratios)))))
.height(minWidth / Math.min.apply(Math, ratios));
});

The reason why I have chosen to use Math.floor() is because of possible rounding errors when integral pixel values are eventually computed.

It is safer to underestimate the width of individual items

The maximum difference is technically half a pixel, since we are rounding up/down) such that the row does not overflow, causing floated elements to be pushed onto a new line and breaking the layout.

… and voila, you’re done! A working demo of the code is available on CodePen.

Extending into a WordPress theme function

It is also possible to extend what has been demonstrated above into a function that is inserted into the functions.php file of a WordPress theme. This allows me to use a selected shortcode, say, [photoset], to create the necessary markup.

Basic design principles

The shortcode will accept the following attributes:

  • id — specify the ID of the WordPress image attachment
  • layout—a comma-separated field value indicating the number of images included per row

An example usage will be:

[photoset id="12,15,17,26,46,45" layout="1,3,2"]

This tells the function to produce a photoset with 1 photo on the first row, 3 photos on the second and 2 photos on the third. Of course I could design a whole new GUI from ground-up that resembles that of Tumblr’s photoset feature, but I don’t see the need to do so if one can correctly sum the number of images in their photoset and dictate the layout manually (and correctly). Therefore, it is important that the sum of the comma-separated values in the layout field matches the total number of IDs specified.

The code

The tricky part of the code is simply to use PHP to parse the layout into a machine-friendly form. Also, probably due to bloated code, there is no easy way of accessing all attributes of image attachments in WordPress through a single function — in this case, I would like to get my hands on:

  • A lower resolution image as the thumbnail. Medium is a good size.
  • The original link to a higher resolution image — the Large size or the actual image file would suffice.
  • Metadata of the attachment, like title, caption, description and etc.

Luke over at the WordPress forum proposed a useful way of fetching image metadata — I have adapted part of his code in my WordPress function.

Much more to yearn for

This segment only serves as a demonstration of how to integrate the markup for photoset into a WordPress function accessible via shortcode through the WYSIWYG editor. Following my guilty admission that I am not a WordPress wizard, I am sure that there are many ways out there of which my code can benefit tremendously from supplementation, improvements and adaptations.

There are many ways of accessing attachment resources in WordPress, ranging from the very basic and essential image URLs, and to image metadata, description, caption and more. Unfortunately WordPress has created a handful of different (and royally confusing) functions to access so, and due to varied personal preferences I will leave it up to you, my reader who doubles as an avid programmer and kick-ass designer, to pick your poison.

You may even modify my code such that you can specify custom URLs to be associated with each image by creating and reading from a new attribute, say, “urls”.

Further improvements

I have been humbled by the reception this piece of writing has received since it was published. This has also spurred me to improve on the demo. One thing that was initially placed on the backburned was brought back to the drawing board — of forcing square thumbnails when the screen becomes narrower, and then forcing the floats to clear each other completely (therefore forcing images onto each line) when screen width reaches that of the mobile phone.

In this improved version of responsive photoset (see full demo, too), it is now mobile friendly, and offers a square thumbnail grid when viewed on tablets (where portraits may appear too small).

The major changes in this version include:

  • Use the meta tag to force width to device width and to set initial scale to 1 in the header element
  • Detecting viewport width upon firing of the resize() event
  • Forcing square thumbnails between screen sizes between 480 and 768px
  • Forcing floats to clear when screen sizes dips below 480px.
  • Hiding <img> elements on screen sizes larger than 480, and reassigning source image to the background-image for better control (especially during square thumbnail displays). This allows me to take advantage of the background-size: cover property.

Closing note

Creating a photoset is complicated, but only in a mathematical sense — once you have figured out how a minimum height can be easily derived from the proportions of individual elements as well as the actual width of the parent container, nothing will hold you back.

While I have tried my very best to check and correct for errors in my code, the code might not be efficient or perfect — it is up to your personal discretion to use and adapt my code to your liking. Leave a note if you know there are places where the code can be improved.

--

--

Terry Mun
Coding & Design

Amateur photographer, enthusiastic web developer, whimsical writer, recreational cyclist, and PhD student in molecular biology. Sometimes clumsy. Aarhus, DK.