Create a Responsive Masonry Gallery

Learn how to create a responsive masonry gallery (with light-box) for your custom WordPress theme, without installing yet another plug-in on your site. We’ll use off-the-shelf libraries to handle the masonry grid and the light-box (pop-up images and sliding gallery) and bring them together in our WordPress theme with just a bit of PHP.

We’ll be making a gallery post grid that looks like this…

masonry gallery - book covers
Responsive masonry gallery

You can see working example on our Flipsum Ipsum website, where we use the same Masonry library to layout the posts on the front page.

How It’ll Work

Before we start writing code, we need to be clear on how we’re actually going to do it. We could look at custom post types, or adding post meta to images in the media library. But what I wanted was something super-simple and elegant… so if we switch-off our new code the site will still work. The layout just won’t be in a masonry view.

The approach we’re going with here is this:

  • Create a post Category called something like “Galleries” with the slug, “galleries”.
  • Create child categories, which will be your on-screen galleries.
  • If you want a post to show up in a gallery, just add it to the relevant category.

This approach should work well for travel bloggers and photographers. The blogger doesn’t have to do anything extra, or learn any new WordPress skills to post their blog entries to a gallery page.

infoBefore starting this tutorial, make sure your site is using a custom child theme, because we’re going to modify its function.php file.

Laying it Out

We’re going to use some PHP, some JavaScript and CSS styles, and rather than just chuck all these into our custom child theme’s folder, let’s keep things clean and well structured.

Create a new folder in your custom child theme called “hw-gallery“, then create three empty text files in there so it looks like this:

masonry gallery file list
The starting point for our gallery module

Now go into your custom child theme’s functions.php and add the following:

// Masonry gallery.
require_once 'hw-gallery/hw-gallery.php';

Check that you’ve not accidentally broken anything by reloading a page on your site. You should be fine – it’s not like we’ve done very much yet.

The file hw-gallery.php is where we’re going to do most of the work. It’s going to have several functions in there, and two of these can be considered critical:

  • hwg_is_gallery() We should be able to call this from anywhere and it’ll return true if the current page is a gallery page. If the page is a gallery page then we need to include the JavaScript and CSS files, but there’s no need to include them on every single page on the site. This should help reduce any additional load we might put on our website’s page-load-speed.
  • hwg_main_loop() We’ll call this at the point where the main content of the page gets rendered. This is the tricky bit, because it’s different for every theme. In short, though, we’re going to make a copy of the parent theme’s archive.php file, and place this copy in our custom child theme (so it gets used instead of the parent theme’s version) and then make a small adjustments to it.

Create some Test Posts

You’re going to need some sample posts in your site so we can test the new software as we go. In my dummy website, I’ve created some post categories like this:

gallery post category structure
Post categories

The idea being that every category sitting beneath the Galleries category will use our responsive masonry gallery, with URLs like this:

https://example.org/category/galleries/book-covers/

External Dependencies

The off-the-shelf components we’re going to use are:

Both of these are distributed as small JavaScript files, so we could just add something like this to our theme…

<!-- Masonry - External JavaScript library -->
<script src="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>

…which would reference the Masonry library from an external server. But we’re going to serve the scripts ourselves. This means we can control the minification and browser caching – don’t worry if you don’t know what these are though. It means we can keep our website really fast, instead of relying on external servers that we’ve got no control over.

Go to each of these sites, download the latest versions, and place them in the hw-gallery folder. The new list of files should look like this.

hw-gallery assets
Masonry and FSLightbox added to our module

Let’s Write some Code

OK, that’s enough faffing about. Let’s get our hands dirty and write some software. Open the file hw-gallery.php and paste this lump into it. This is the entire working PHP file, although it won’t actually work properly until we’ve populated the JS and CSS files too.

<?php
/**
 * Masonry gllery layout for Category pages.
 *
 * include this file from your custom child them:
 *
 *    require_once 'hw-gallery/hw-gallery.php'
 *
 * Define the root category slug that's the parent of all gallery categories.
 * If you don't define this, we default to looking for a category with the
 * slug 'galleries'.
 *
 *    const HWG_ROOT_GALLERY_CATEGORY_SLUG = 'my-photo-galleries';
 *
 * More settings:
 *
 *    const HWG_GALLERY_IMAGES_PER_PAGE = 50;
 *    const HWG_GALLERY_IMAGE_SIZE = 'medium';
 *    const HWG_IS_LEGEND_ENABLED' = true;
 *
 * There are also some filters in here, if you want to make per-gallery
 * adjustments in your theme.
 *
 */

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');
}

function hwg_enqueue_scripts() {
	if (hwg_is_gallery()) {
		$theme_version = wp_get_theme()->get('Version');
		$theme_base_uri = get_stylesheet_directory_uri();
		$this_dir_name = basename(dirname(__FILE__));
		$base_uri = $theme_base_uri . '/' . $this_dir_name;

		// FSLightbox
		wp_enqueue_script(
			'hwg-fslightbox-js',
			$base_uri . '/fslightbox.js',
			array('jquery'),
			$theme_version,
			true
		);

		// Masonry
		wp_enqueue_script(
			'hwg-masonry-js',
			$base_uri . '/masonry.pkgd.js',
			array('jquery'),
			'4.2.2',
			true
		);

		// Our JS to initialise things after page-load.
		wp_enqueue_script('hwg-js',
			$base_uri . '/hw-gallery.js',
			array('hwg-masonry-js', 'hwg-fslightbox-js'),
			$theme_version,
			true
		);

		// Styles to support the masonry view.
		wp_enqueue_style('hwg-masonry-css',
			$base_uri . '/hw-gallery.css',
			false,
			$theme_version
		);
	}
}
add_action('wp_enqueue_scripts', 'hwg_enqueue_scripts');

function hwg_is_gallery() {
	global $hwg_is_this_a_gallery;

	if (!isset($hwg_is_this_a_gallery)) {
		$hwg_is_this_a_gallery = false;
		$root_gallery_slug = 'galleries';

		if (defined('HWG_ROOT_GALLERY_CATEGORY_SLUG')) {
			$root_gallery_slug = HWG_ROOT_GALLERY_CATEGORY_SLUG;
		}

		$root_gallery_slug = apply_filters(
			'hwg_root_gallery_category_slug',
			$root_gallery_slug
		);

		if (!is_category()) {
			// This isn't a category page, so it can't be a gallery page.
		} elseif (empty($root_gallery_slug)) {
			// We don't have a root gallery slug.
		} elseif (empty($galleries_category = get_category_by_slug($root_gallery_slug))) {
			// Couldn't find the specified root gallery.
		} else {
			$this_category = get_queried_object();

			$hwg_is_this_a_gallery = cat_is_ancestor_of(
				$galleries_category->term_id,
				$this_category->term_id
			) || is_category($galleries_category->term_id);
		}

		// Allow the setting to be overridden.
		$hwg_is_this_a_gallery = boolval(
			apply_filters(
				'hwg_is_this_a_gallery',
				$hwg_is_this_a_gallery
			)
		);
	}

	return $hwg_is_this_a_gallery;
}

function hwg_pre_get_posts($query) {
	if (hwg_is_gallery()) {
		$posts_per_page = 0;

		if (defined('HWG_GALLERY_IMAGES_PER_PAGE')) {
			$posts_per_page = intval(HWG_GALLERY_IMAGES_PER_PAGE);
		}

		if ($posts_per_page > 0) {
			$query->set('posts_per_page', $posts_per_page);
		}
	}
}
add_action('pre_get_posts', 'hwg_pre_get_posts');

function hwg_main_loop() {
	if (have_posts()) {

		// Set up some properties that will apply to every item in the loop.
		if (!defined('HWG_GALLERY_IMAGE_SIZE')) {
			define('HWG_GALLERY_IMAGE_SIZE', 'medium');
		}

		if (!defined('HWG_IS_LEGEND_ENABLED')) {
			define('HWG_IS_LEGEND_ENABLED', true);
		}

		$classes = apply_filters('hwg_grid_open_classes', array('masonry-grid'));

		// Open the masonry grid.
		printf('<div class="%s">', implode(' ', $classes));

		while (have_posts()) {
			the_post();

			hwg_show_post();
		}

		// Close the masonry grid.
		echo '</div>'; // .grid
	}
}

/**
 * If you want to render the grid items yourself, just provide your own
 * hwg_show_post() in your custom child theme's functions.php file.
 */
if (!function_exists('hwg_show_post')) {
	function hwg_show_post() {
		$gallery_name = 'main-gallery';
		$main_image_url = get_the_post_thumbnail_url(null, 'full');
		$classes = apply_filters('hwg_grid_item_classes', array('grid-item'));
		$is_legend_enabled = boolval(HWG_IS_LEGEND_ENABLED);

		// Open the masonry grid item.
		printf('<div class="%s">', implode(' ', $classes));

		printf('<a class="hwg-item" href="%s" data-fslightbox="%s" data-alt="%s">',
			esc_url($main_image_url),
			esc_attr($gallery_name),
			esc_attr(get_the_title()),
		);

		the_post_thumbnail(HWG_GALLERY_IMAGE_SIZE);

		// The overlay caption.
		if ($is_legend_enabled) {
			echo '<div class="post-legend">';
			the_title();
			echo '</div>'; //.post-legend
		}

		printf('</a>');

		// Close the masonry grid item.
		echo '</div>'; //.grid-item
	}
}

Here’s an overview of what we’re actually doing:

  1. We set up a handler for the wp_enqueue_scripts hook, which gets called by WordPress when it needs to know what scripts and stylesheets to load.
  2. If hwg_is_gallery() returns true then enqueue:
    1. FSLightbox
    2. Masonry
    3. Our JavaScript file
    4. The CSS stylesheet

infoWe’ve enqueued our JS and CSS files in the site footer, rather than the header, to keep the page-load-speed good.

Then we just have to wait until someone calls the hwg_mainLoop() function.

Calling the Main Loop Function from the Theme Template

This is the tricky bit, because it’s dependent on the parent theme you’re using, and you can call it multiple ways. We’re going to use a process that’s fairly robust – and is the whole reason we use a custom child theme in the first place.

In this example, we’re using the Astra WordPress theme as our parent theme, but the process should be similar with other parent themes.

Go to your parent theme’s main folder and look for category.php. If it doesn’t exist then look for archive.php instead. Copy category.php (or archive.php) into your custom child theme’s folder. We’ll assume that category.php does NOT exist and archive.php DOES exist, but the following steps are the same if you’re using category.php instead.

Edit the archive.php file that’s in your custom child theme’s folder.

dangerBe careful not to accidentally edit the archive.php file that’s in your parent theme’s folder by mistake.

Most archive.php files will call a function that processes the main WordPress query in a loop. In Astra, the function is called astra_content_loop(). We need to find this and create an if-the-else statement around it. If you need help figuring this out for your theme, then hit me up in the comments below.

astra_primary_content_top();

astra_archive_header();

// If this is a gallery page then call hwg_main_loop() to process
// the posts, otherwise let Astra do it normally.
if (function_exists('hwg_is_gallery') && hwg_is_gallery()) {
	hwg_main_loop();
} else {
	astra_content_loop();
}

astra_pagination();

astra_primary_content_bottom();

That’s the hardest bit of this project. The rest is plan sailing.

Have a quick check that your site’s not broken before doing the next bit. Your gallery category pages will look weird right now, but not for long.

Initialise the Masonry Gallery

Now we’ve got a page with the right HTML in there, we need to fire-off a bit of JavaScript in the browser to initialise Masonry. The “Getting Started with Masonry” guide says we can do this several ways, and we’re going to do it with jQuery. It’s tempting to do it in HTML so we don’t have to include a hw-gallery.js file at all, but it’s a bit fiddly to debug. The jQuery way of doing it keeps things cleaner, and we can do more with it in the future if we want to.

Edit the file hw-gallery.js and paste the following into it.

/*
 * hw-gallery.js
 */

(function($) {
    'use strict';

    $(window).on('load', function() {
        $('.masonry-grid').masonry({
            itemSelector: '.grid-item',
            percentPosition: true
        })
    });
})(jQuery);

Not much to it – we’re just calling the masonry() function on all elements in the DOM that have the “masonry-grid” CSS class applied to them. There will only be one element with the “masonry-grid” class, which is the outer <div> that wraps our gallery.

Styling our Responsive Masonry Gallery

Edit the file hw-gallery.css and paste the following into it:

/*
 * hw-gallery.css
 */

.grid-item,
.grid-item--width2 {
    border: 0.5rem solid transparent;
}

.grid-item .wp-post-image {
    width: 100%;
    border-radius: 0.25rem;
    border: 1px dotted grey;
    box-shadow: 0 0 0.5rem grey;
    transition: 0.3s;
}

.grid-item .hwg-item {
	position: relative;
	display:  block;
}

.grid-item .hwg-item:hover .wp-post-image {
    border: 1px dotted black;
    box-shadow: 0 0 0.5rem grey, 0 0 0.5rem grey;
}

.grid-item {
    width: 50%;
}

.grid-item--width2 {
    width: 100%;
}

@media (min-width: 576px) {
    .grid-item {
        width: 25%;
    }

    .grid-item--width2 {
        width: 50%;
    }
}

@media (min-width: 2000px) {
    .grid-item {
        width: 20%;
    }

    .grid-item--width2 {
        width: 100%;
    }
}

.grid-item .post-legend {
	position: absolute;
	bottom:  0;
	border-top:  1px solid rgba( 0, 0, 0, 0.25 );
	background-color:  rgba( 64, 64, 64, 0.50 );
	display:  block;
	width:  100%;
	text-align: center;
	color:  rgba( 255, 255, 255, 0.80 );
	font-weight: bold;
	font-size: 90%;
	letter-spacing: 0.05em;
	text-shadow: 0 0 1px black;
	padding:  0.5em 0 0.5em 0;
	transition: 0.20s;
}

.grid-item .hwg-item:hover .post-legend {
	background-color:  rgba( 32, 32, 32, 0.75 );
	color: white;
}

This contains enough styling to get us up-and-running.

warningIt’s important that you don’t make changes to hw-gallery.css. If you want to customise the styling then override the styles in your custom child theme’s style.css file instead. We want to keep all the files in our hw-gallery folder generic, so we can use them in other WordPress projects in the future.

The styles you’ll most likely want to look at are grid-item and grid-item–width2. We don’t actually use grid-item–width2 in our module, but it’s there so you can extend the PHP if you want to add double-column images.

Finishing Up

It should all work now. Reload one of your gallery category pages (bypass the browser cache) and have a look. If it’s not working, then break the problem down into small chunks and find the root cause.

  • If it looks like the module isn’t working at all, check that hwg_is_gallery() is returning true. If it’s returning false then maybe there’s a spelling mistake in your category slug?
  • If you’re getting JavaScript errors then comment-out the three JS files in hwg_enqueue_scripts() and see if you can pin the problem down to a particular file.
  • Test that the layout works well on different size displays, and have a good ol’ play with it.

Although this would make a cool starting point for a bigger project, I like that it’s not very big. It’s a svelte bit of software. We don’t make any database calls and our CSS/JS are only included on gallery pages – not on every page of the site. This is an elegant, lightweight way of adding a really nice UX enhancement to any WordPress site. All you need to do is figure out how to call hwg_main_loop() from your theme’s archive.php (or category.php) file.

Let me know of any interesting uses you find for this little module 🙂

2 thoughts on “Create a Responsive Masonry Gallery”

  1. Thanks for a great tutorial. I really like your approach. However, I couldn’t get the masonry working on my test site. I think I understood everything – even spotted a typo in your hw-gallery.js (.masony-grid). By commenting to the archive.php (mine is Astra’s as well) I could see that the hwg_is_gallery function returns false. I’m thinking I didn’t catch the idea of what slugs/urls this approach should catch. Sub-categories of a parent category, the parent category being the root gallery category slug?
    My use case would be to show the archive page of a custom post type as masonry, categorised or not. How would that change the approach?
    I’d be really grateful if you have the time to reply! If not, I’m grateful anyway.

    1. Thanks for spotting the typo. whoops 🙂
      The idea is that you could have a category called “Galleries” (slug=”galleries”) and then all the child categories underneath the Galleries will be treated as galleries.

      If you want to use a custom post type, you could hook the hwg_is_this_a_gallery filter like this in your functions.php file…

      function custom_hwg_is_this_a_gallery($is_a_gallery) {
      if (is_a($queried_object = get_queried_object(), ‘WP_Post_Type’)) {
      $is_a_gallery = ($queried_object->name == ‘my-post-type’);
      }
      return $is_a_gallery;
      }
      apply_filters(‘hwg_is_this_a_gallery’, ‘custom_hwg_is_this_a_gallery’);

      Just replace ‘my-post-type’ with the name of your post type. I’ve not tested this snippet, but if you look in hw-gallery.php for ‘hwg_is_this_a_gallery’ you’ll see the filter.

      Let me know how you get on.
      Paul

Leave a Comment

Your email address will not be published. Required fields are marked *