The WordPress Masonry Gallery Tutorial

In this copy-and-paste tutorial we’ll convert the standard WordPress Gallery Block into a responsive masonry gallery, complete with a full-screen light box. We’ll do this without installing any plugins, so there’s no nagware, annoying admin banners, or any other bloat.

infoClick on the images to see the responsive lightbox working 😎

importantMake sure you’re using a custom child theme so you can edit functions.php.

Define the Requirements

Here’s what we want to achieve…

  • No new plugins.
  • We want to be able to just drop the code into any WordPress child theme we want, so we can use it in other projects.
  • Add the masonry-gallery CSS class to any WordPress gallery block and it’ll magically become a responsive masonry gallery.
  • When the gallery images are clicked, pop up into a responsive light box, with swipe left/right and Esc to close.
WordPress masonry gallery CSS class
CSS Class for our masonry gallery

Break Down the Problem

infoComplicated Bit: If you just want to make it work, jump to the code.

From the requirements, it looks like there’s a lot to sort out, but the complicated stuff has already been written by other people. All we need to do is stitch-together a couple of JavaScript libraries and write some code to output the HTML. We’re going to use a similar technique to our WordPress Gallery Slideshow tutorial – hooking the render_block filter to detect when WordPress is trying to render a Gallery block with the masonry-gallery CSS class.

The back-end PHP logic will work like this…

  • In the backend of the site (in PHP), hook the render_block filter
  • If WordPress is trying to render a gallery block, and it has the masonry-gallery CSS class then…
    • Enqueue the frontend assets for Masonry and fslightbox.js (third-party JavaScript libraries)
    • Enqueue our frontend JavaScript and CSS files
    • Open a container <figure> for the gallery, with some special markup for Masonry
    • For each image in the gallery…
      • Open a <figure> for this image
      • Add the <img> to the HTML output
      • Close the <figure> for this image
    • Close the <figure> for the gallery
  • Return the HTML

The JavaScript Logic

The documentation for Masonry says it’s pretty easy to initialise a masonry gallery. And… it is… sort of. But… The problem is that the script loads and initialises the masonry gallery before all the images have finished loading. So Masonry has trouble laying out the images, because it doesn’t know their dimensions (yet). When the images finish loading, everything looks a bit of a mess. The docs say you can use a library called imagesLoaded to fix this, but we’re trying to reduce dependencies here, so we’ll do it ourselves.

We’re going to use a technique called debouncing, and it works like this:

  • Find all the image elements in our gallery, using querySelectorAll()
  • For each image…
    • Add an img load event listener, which will call a function called maybeLayoutMasonryGalleries() after the image has loaded

The maybeLayoutMasonryGalleries() function will call layoutMasonryGalleries() after a delay of something like 100ms (a tenth of a second), UNLESS maybeLayoutMasonryGalleries() is called again in the meantime, in which case it’ll cancel the previous (delayed) call to layoutMasonryGalleries() and try again, after another delay of 100ms.

Without debouncing: if a gallery has 25 images in it, it’s going to call layoutMasonryGalleries() 25 times. This could cause the browser to slow down and stutter.

With debouncing: if each image finishes loading within 100ms of another image having loaded, we’ll only call layoutMasonryGalleries() once… 100ms after the final image has finished loading.

It’s a useful technique for responding to a sequence of events, when you don’t know how many events there will be, or how frequently they’ll fire.

Scaffold the Project

Before we can write any actual code, we need to set things out. In your custom child theme, create an empty text file called “wpt-masonry-gallery.php” and new folder called “wpt-masonry-gallery”. Go into the wpt-masonry-gallery folder and create two new files, called “wpt-masonry-gallery.css” and “wpt-masonry-gallery.js”.

Download masonryjs button
Download Masonry

Save this file into your “wpt-masonry-gallery” folder as masonry.pkgd.min.js

Download Button for FS Lightbox
Download fslightbox.js

Extract the zip file and copy fslightbox.js into your wpt-masonry-gallery folder.

You should end up with this in your wpt-masonry-gallery folder

List of asset files in the masonry gallery tutorial
File assets for the WPT Masonry Gallery Tutorial

Next, open your child theme’s functions.php file and paste the following into it:

// Masonry gallery
require_once dirname(__FILE__) . '/wpt-masonry-gallery.php';

Reload some content on our site to make sure nothing’s broken.

The PHP Code

The back-end PHP code is quite lightweight. Open wpt-masonry-gallery.php and paste the following into it:

<?php

/**
 * WP Tutorials : Masonry Gallery (WPTMG)
 *
 * https://wp-tutorials.tech/refine-wordpress/the-wordpress-masonry-gallery-tutorial/
 */

defined('WPINC') || die();

const WPTMG_CLASS_NAME = 'masonry-gallery';
const WPTMG_BLOCK_NAME = 'core/gallery';
const WPTMG_DEFAULT_IMAGE_SIZE = 'full';
const WPTMG_DEBOUNCE_INTERVAL = 50;
const WPTMG_EVERY_NTH_LARGE_IMAGE = 9;
const WPTMG_MASONRY_VERSION = '4.2.2';
const WPTMG_FSLIGHTBOX_VERSION = '3.3.1';

function wptmg_enqueue_assets() {
	global $wptmg_have_styles_been_enqueued;

	if (is_null($wptmg_have_styles_been_enqueued)) {
		$base_uri = get_stylesheet_directory_uri() . '/';
		$version = wp_get_theme()->get('Version');
		$handle = 'wptmg';

		wp_enqueue_script(
			'masonry',
			$base_uri . 'wpt-masonry-gallery/masonry.pkgd.min.js',
			null, // No script dependencies
			WPTMG_MASONRY_VERSION
		);

		wp_enqueue_script(
			'fslightbox',
			$base_uri . 'wpt-masonry-gallery/fslightbox.js',
			null, // No script dependencies
			WPTMG_FSLIGHTBOX_VERSION
		);

		wp_enqueue_style(
			$handle,
			$base_uri . 'wpt-masonry-gallery/wpt-masonry-gallery.css',
			null, // No style dependencies
			$version
		);

		wp_enqueue_script(
			$handle,
			$base_uri . 'wpt-masonry-gallery/wpt-masonry-gallery.js',
			array('masonry', 'fslightbox'), // Load our script after fslightbox and masonry
			$version
		);

		// Pass our configuration to the frontend in a global JavaScript object
		// called wptmgData.
		wp_localize_script(
			$handle,
			'wptmgData',
			array(
				'gallerySelector' => '.wpt-masonry-gallery',
				'colWidthSelector' => '.wpt-gallery-image--width1',
				'itemSelector' => '.wpt-gallery-image',
				'imgSelector' => '.wpt-gallery-image img',
				'debounceInterval' => WPTMG_DEBOUNCE_INTERVAL,
			)
		);

		$wptmg_have_styles_been_enqueued = true;
	}
}

function wptmg_render_masonry_gallery(string $block_content, array $block) {
	if (is_admin() || wp_doing_ajax()) {
		// We're not in the front-end.. Don't do anything.
	} elseif ($block['blockName'] != WPTMG_BLOCK_NAME) {
		// This isn't a WP Gallery block.
	} elseif (!array_key_exists('attrs', $block)) {
		// The gallery block has no attributes.
	} elseif (!array_key_exists('className', $block['attrs'])) {
		// The className attribute is not specified.
	} elseif (empty($class_name = $block['attrs']['className'])) {
		// The className attribute is empty.
	} elseif (empty($classes = array_filter(explode(' ', $class_name)))) {
		// The className attribute is empty.
	} elseif (!in_array(WPTMG_CLASS_NAME, $classes)) {
		// "masonry-gallery" isn't in the list of CSS Classes for the block.
	} elseif (empty($inner_blocks = $block['innerBlocks'])) {
		// The gallery has no image blocks in it.
	} else {
		wptmg_enqueue_assets();

		global $wptmg_gallery_index;
		if (is_null($wptmg_gallery_index)) {
			$wptmg_gallery_index = 1;
		}
		$gallery_name = 'wpt-gallery-' . $wptmg_gallery_index;

		$block_content = sprintf('<figure class="wpt-masonry-gallery %s">', WPTMG_CLASS_NAME);

		// Loop through the inner image blocks and pull out the image IDs.
		$image_index = 0;
		foreach ($inner_blocks as $inner_block) {
			if ($inner_block['blockName'] != 'core/image') {
				// ...
			} elseif (($image_id = intval($inner_block['attrs']['id'])) <= 0) {
				// ...
			} else {
				$image_size = WPTMG_DEFAULT_IMAGE_SIZE;
				if (array_key_exists('sizeSlug', $inner_block['attrs'])) {
					$image_size = $inner_block['attrs']['sizeSlug'];
				}

				$thumbnail_url = wp_get_attachment_image_url($image_id, $image_size);
				$fullsize_url = wp_get_attachment_image_url($image_id, 'full');
				$image_alt = get_post_meta($image_id, '_wp_attachment_image_alt', true);

				$colspan = 1;

				// Every nth image should be bigger than the rest.
				if ($image_index % WPTMG_EVERY_NTH_LARGE_IMAGE == 0) {
					$colspan = 2;
				}

				$link_props = sprintf('data-fslightbox="%s"', esc_attr($gallery_name));

				$image_props = '';
				if (!empty($image_alt)) {
					$image_props .= sprintf(' alt="%s"', esc_attr($image_alt));
				}

				$block_content .= sprintf(
					'<figure class="wpt-gallery-image wpt-gallery-image--width%d"><a href="%s" %s><img src="%s" %s/></a></figure>',
					$colspan,
					esc_url($fullsize_url),
					$link_props,
					esc_url($thumbnail_url),
					$image_props
				);

				++$image_index;
			}
		}

		$block_content .= '</figure>'; // .wpt-masonry-gallery

		++$wptmg_gallery_index;
	}

	return $block_content;
}
add_filter('render_block', 'wptmg_render_masonry_gallery', 50, 2);

Notice how we use wp_localize_script() to pass parameters from the back-end into the JavaScript frontend. This lets us keep all our parameters in the PHP file – even the parameters that control how the JavaScript stuff works.

The core of this project is just a loop that runs through all of the gallery images and generates HTML. Each gallery image is a <figure> that contains an <img> element. The width of an image (one or two columns wide) is controlled by the CSS class we assign to the <figure> element. We use wpt-gallery-image--width1 for most of the images and wpt-gallery-image--width2 for the images that span two columns.

Add Some Style

Open wpt-masonry-gallery.css and paste the following into it. You might want to change the media selector to suit the break point for your theme, but 922px should work well for most cases.

/**
 * WP Tutorials : Masonry Gallery (WPTMG)
 *
 * https://wp-tutorials.tech/refine-wordpress/the-wordpress-masonry-gallery-tutorial/
 */

.wpt-masonry-gallery {
	/* |The gallery container */
}

.wpt-gallery-image {
	padding: 0.25em;
}

.wpt-gallery-image,
.wpt-gallery-image a,
.wpt-gallery-image img {
	display: block;
}

.wpt-gallery-image--width1,
.wpt-gallery-image--width2 {
	width: 100%;
}

@media(min-width: 922px) {
	.wpt-gallery-image--width1 {
		width: 33.33%;
	}

	.wpt-gallery-image--width2 {
		width: 66.66%;
	}
}

The JavaScript Code

The final chunk of code is what makes it all work in the browser. Open wpt-masonry-gallery.js and paste the following into it.

/**
 * WP Tutorials : Masonry Gallery (WPTMG)
 *
 * https://wp-tutorials.tech/refine-wordpress/the-wordpress-masonry-gallery-tutorial/
 */
document.addEventListener('DOMContentLoaded', function() {
	'use strict';

	if (typeof wptmgData !== 'undefined') {
		// Uncomment this to confirm we've loaded correctly.
		// console.log('Masonry Gallery : Load');

		var masonryGalleries = [];
		var layoutTimeoutHandle = null;

		/**
		 * Find all figure elements that have our masonry-gallery CSS class and
		 * loop through them.
		 */
		document.querySelectorAll(wptmgData.gallerySelector).forEach(function(figure) {
			var galleryArgs = {
				columnWidth: wptmgData.colWidthSelector,
				itemSelector: wptmgData.itemSelector,
				percentPosition: true
			};

			// Create the masonry gallery object.
			var masonryGallery = new Masonry(figure, galleryArgs);

			// Store the new gallery object in an array so we can access it later.
			masonryGalleries.push(masonryGallery);
		});

		/**
		 * Find all img elements that belong to our masonry galleries and start
		 * listening for the onload events.
		 */
		document.querySelectorAll(wptmgData.imgSelector).forEach(function(img) {
			img.addEventListener('load', function() {
				maybeLayoutMasonryGalleries();
			});
		});

		/**
		 * Debounce calls to layoutMasonryGalleries() by running thrm through
		 * maybeLayoutMasonryGalleries() first.
		 */
		function maybeLayoutMasonryGalleries() {
			// Clear the previous in-process timeout.
			if (layoutTimeoutHandle) {
				clearTimeout(layoutTimeoutHandle);
				layoutTimeoutHandle = null;
			}

			// Start a new timeout.
			layoutTimeoutHandle = setTimeout(
				layoutMasonryGalleries,
				wptmgData.debounceInterval
			);
		}

		/**
		 * Layout (or re-layout) all the masonry galleries.
		 */
		function layoutMasonryGalleries() {
			// Uncomment to confirm that we aren't being called too many times.
			// console.log('Layout masonry galeries now');

			layoutTimeoutHandle = null;
			masonryGalleries.forEach(function(masonryGallery) {
				masonryGallery.layout();
			});
		}
	}

});

Wrap Up, Test & Extend

Just add a Gallery Block to your content, set its CSS class to masonry-gallery and add some images. We’ve got no jQuery in there, and all we had to do to make the lightbox work was add data-fslightbox="wpt-gallery-X" into the HTML.

If you want to make tweaks and extend the code, have a go at some of these:

  • Alter which images span 2 columns by changing how WPTMG_EVERY_NTH_LARGE_IMAGE is used.
  • Tinker with the .wpt-gallery-image class to adjust the image layout.
  • Add some extra HTML after the <img> but before </figure> to render image captions.

Have fun with your galleries 😎👍

Like This Tutorial?

loading

Let us know

8 thoughts on “The WordPress Masonry Gallery Tutorial”

  1. Avatar

    Hi. I really like the way the masonry gallery looks on this page. I’m trying to modify it to work with ACF so I can have a masonry gallery with images the client selects in the backend but haven’t gotten that to work yet. One thing I did notice is that the last line in the PHP code you provide appears to have a typo in it… on lines 73 and and 152 you wrote “gallert” instead of “gallery” – though since they both are the same, I don’t think it affects the code. Maybe that was intentional, but I thought I’d mention it in case it wasn’t.

    1. Avatar

      Hi. I’m glad you like the tutorial. I tried to write it so it could be modified to work with an array of image Ids… which you could pull from anywhere. The JavaScript should work without you having to change it.

      Well spotted on the typo. Sometimes I leave them in there so I can search the web and see who has used the code in their projects, but I think I’ll fix this one because it does look a bit silly. Cheers 👍

  2. Avatar

    Hi there! Thanks so much for putting this tutorial out here! I know there were recent changes to the gallery block and just wanted to confirm if this still works? Mine is having some issues with both the images displaying at the right sizes and getting the lightbox to load…all files are loading just fine so was wondering if it’s something else. Thanks in advance for any input!

    1. Avatar

      Hi!

      This tutorial works with the “new way” that gallery image blocks work, which started around WordPress 5.9.1, I think it was. This tutorial won’t work with older versions of WordPress, but you should always keep WP core up-to-date.

      To chase down problems like this, where the issue could be in the PHP or in the JavaScript, I always install the “Query Monitor” plugin – very useful. That said, this tutorial was written and tested using a child theme that has Astra as its parent. If you’re using a different parent theme, maybe I need to reproduce it here to see if there’s a fix?

      Which parent theme are you using? Or maybe you’re using something like Elementor (I’ve not tested against Elementor)?

      1. Avatar

        Hi, great post. What if im wanna use glightbox instead?
        I have already embed ed it and wanna keep the same ligjtbox for all. Thanks a lot for your kind answer.

        1. Avatar

          Hi
          I looked at GLightbox for you. It would be easy to replace FSLightbox with GLightbox, but it would be messy to make the tutorial support both of them. Changing the HTML markup for GLightbox is quite easy, but I think the JavaScript will be weird.
          If you really want GLightbox, send a PM from the “Send us an email” page and we’ll see what we can create.

  3. Avatar

    This article was well written, informative and very generous of you to share.
    I tried sending an email (from the send us an email button) but got an error. Also tried to send from https://headwall-hosting.com but got exactly the same error. Is there another email that works? Or can you contact me using my email associated with this msg?
    Thanks in advance.
    Frank

Leave a Comment

Your email address will not be published.