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.
tipClick 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.
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
- Open a
- 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
- Add an img load event listener, which will call a function called
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”.
Save this file into your “wpt-masonry-gallery” folder as masonry.pkgd.min.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
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/ */ /* |The gallery container */ .wpt-masonry-gallery { margin-bottom: 2em; } .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 😎👍
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.
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 👍
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!
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)?
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.
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.
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
Hi Frank – I’ve emailed you directly about this. We’ll connect soon and help get it working for you.
Great Job! I wanted to ask you something. How can I implement the description?
The Pro version of fslightbox can do image captions in the lightbox, and it’s pretty easy. You would just need to add the data-caption=”…” attribute to the anchors. Info is here: https://fslightbox.com/javascript/documentation/captions and the cost is reasonable, I think.
Another option would be to replace fslightbox with something like glightbox – that can do captions. The tutorial would need to be restructured to do that though.