Turn any WordPress Gallery into a Slide Show

Gallery to Slideshow tutorial

This tutorial covers how to convert the standard WordPress gallery block into a fade-through/animated slide show. We’ll use PHP to render the HTML, and core JavaScript in the browser (not jQuery) – it’s super-lightweight.

What we want to be able to do is define a regular gallery in the WordPress Block Editor, but give it a special CSS class… like “slideshow”. Our code will pick up on this and override the HTML for these slide show galleries, using our own markup. We’ll add bit of CSS and JavaScript to make it work in the browser.

We want to reduce reliance on dependencies, so we won’t use jQuery animations to fade through the slides… we’ll use standard CSS transitions for that.

social network icons on a phone
motion
carousel
fork in the road

Slide show from a standard WP Gallery Block

infoUpdated 5th February 2022 to handle the new gallery block HTML in WordPress 5.9.

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

Figure Out the Logic

To make it work, we’ll add the CSS class “current-slide” to whichever slide is meant to be visible. When we want to make the next slide visible, we’ll remove “current-slide”, then add it to the CSS classes of the next slide we want to show. The fade-through effect will “just work” because of the CSS transitions. See how current-slide moves from one figure to the next, in sequence.

<div class="slideshow">
	<figure class="current-side">
		<img src="..." />
	</figure>
	<figure class="">
		<img src="..." />
	</figure>
	<figure class="">
		<img src="..." />
	</figure>
</div>

Show Slide #1 (slideIndex=0)

<div class="slideshow">
	<figure class="">
		<img src="..." />
	</figure>
	<figure class="current-side">
		<img src="..." />
	</figure>
	<figure class="">
		<img src="..." />
	</figure>
</div>

Show Slide #2 (slideIndex=1)

Page Load & Initial State

We need to pay some attention to the initial page-load state of the slide show images.

The page (HTML) will load before our CSS loads, and the HTML is just a series of <img src="..." /> elements. We won’t want these all to flash up on the screen (quickly) and then disappear again when the CSS loads. So… when we render the HTML, we’ll add style="display:none;" to all except the first slide. Then, when our JavaScript initialises the slide show, we can remove the style property from each of the slides because we’ll know that the CSS has loaded by then.

This is (roughly) what our HTML will look like before our JavaScript code runs.

<div class="slideshow">
	<figure class="current-side">
		<img src="..." />
	</figure>
	<figure style="display: none;">
		<img src="..." />
	</figure>
	<figure style="display: none;">
		<img src="..." />
	</figure>
</div>

The PHP Code

Let’s start by setting out our files.

In your custom child theme, create a new folder called “wpt-gallery-to-slideshow”. In this folder, create two empty text files, called “wptgts.css” and “wptgts.js” (wptgts stands for “WP Tutorials, Gallery to Slideshow”). Next, in your child theme’s main folder, create a file called “wpt-gallery-to-slideshow.php”.

  • my-theme/
    • functions.php
    • style.css
    • wpt-gallery-to-slideshow/
      • wptgts.css
      • wptgts.js
    • wpt-gallery-to-slideshow.php

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

// WP-Tutorials Gallery-to-Slideshow
require_once dirname(__FILE__) . '/wpt-gallery-to-slideshow.php';

Now open wpt-gallery-to-slideshow.php and paste the following into it. This is the core of the back-end part of this project:

<?php

/**
 * WP Tutorials : Gallery-to-Slideshow (WPTGTS)
 *
 * https://wp-tutorials.tech/refine-wordpress/turn-wordpress-gallery-into-a-slideshow/
 *
 * Changelog
 * 
 * 2022-02-05 : Adjusted the PHP to handle changes to the core/gallery block.
 */

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

const WPTGTS_CLASS_NAME = 'slideshow';
const WPTGTS_BLOCK_NAME = 'core/gallery';
const WPTGTS_DEFAULT_IMAGE_SIZE = 'full';
const WPTGTS_FIRST_SLIDE_DELAY = 2000; // milliseconds
const WPTGTS_DEFAULT_SLIDE_DELAY = 3000; // milliseconds

function wptgts_enqueue_assets() {
	global $wptgts_have_styles_been_enqueued;

	if (is_null($wptgts_have_styles_been_enqueued)) {
		$handle = 'wptgts';

		$base_uri = get_stylesheet_directory_uri() . '/';
		$version = wp_get_theme()->get('Version');

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

		wp_enqueue_script(
			$handle,
			$base_uri . 'wpt-gallery-to-slideshow/wptgts.js',
			null, // No script dependencies
			$version
		);

		wp_localize_script(
			$handle,
			'wptgtsData',
			array(
				'selector' => '.wptgts.slideshow',
			)
		);

		$wptgts_have_styles_been_enqueued = true;
	}
}

function wptgts_render_slideshow(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'] != WPTGTS_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(WPTGTS_CLASS_NAME, $classes)) {
		// "slideshow" 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 {
		// Loop through the inner image blocks and pull out the image IDs.
		$image_ids = array();
		foreach ($inner_blocks as $inner_block) {
			if ($inner_block['blockName'] != 'core/image') {
				// ...
			} elseif (($image_id = intval($inner_block['attrs']['id'])) <= 0) {
				// ...
			} else {
				$image_ids[] = $image_id;
			}
		}

		wptgts_enqueue_assets();

		$first_duration = apply_filters('wptgts_first_duration', WPTGTS_FIRST_SLIDE_DELAY);
		$slide_duration = apply_filters('wptgts_slide_duration', WPTGTS_DEFAULT_SLIDE_DELAY);

		$image_size = apply_filters('wptgts_image_size', WPTGTS_DEFAULT_IMAGE_SIZE);
		if (array_key_exists('sizeSlug', $block['attrs'])) {
			$image_size = $block['attrs']['sizeSlug'];
		}

		// Open the containing div element.
		$block_content = sprintf(
			'<div data-wptgts-first-duration="%s" data-wptgts-duration="%s" data-wptgts-image-count="%s" class="wptgts %s">',
			esc_attr($first_duration),
			esc_attr($slide_duration),
			count($image_ids),
			esc_attr($class_name)
		);

		$image_index = 0;
		foreach ($image_ids as $image_id) {
			$image_src = wp_get_attachment_image_src($image_id, $image_size);
			$image_url = $image_src[0];
			$image_width = $image_src[1];
			$image_height = $image_src[2];
			$image_alt = get_post_meta($image_id, '_wp_attachment_image_alt', true);
			$slide_classes = '';
			$slide_props = '';
			$image_props = '';

			if ($image_index == 0) {
				// Set the CSS classes of the first slide.
				$slide_classes = 'current-slide';
			} else {
				// All slides that aren't the first slide start with display:none;
				// so they don't temporarily display, while the stylesheet is
				// loading. We'll remove the "style" attribute in JS.
				$slide_props .= ' style="display:none;"';
			}

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

			// Don't lazy-load the first image, because it can harm the LCP metric.
			if ($image_index > 0) {
				$image_props .= ' loading="lazy"';
			}

			// Each slide is an img contained within a figure element.
			$block_content .= sprintf(
				'<figure class="%s" %s><img src="%s" width="%d" height="%d" %s/></figure>',
				esc_attr($slide_classes),
				$slide_props,
				esc_url($image_url),
				$image_width,
				$image_height,
				$image_props
			);

			++$image_index;
		}

		// Close the containing div.
		$block_content .= '</div>'; // .wptgts-slideshow
	}

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

There’s not much going on in here, and the functionality falls into two functions.

wptgts_enqueue_assets() : Enqueue the front-end CSS/JS assets. We’ve put this in a separate function so it can be used by other code in the future – maybe you want to create additional ways of adding slide shows?

The main wptgts_render_slideshow() function is called by hooking the WordPress render_block filter. Whenever WordPress renders a block, it will call our function, so we need to be really careful what we do in here…

wptgts_render_slideshow()

  1. Run a series of simple checks, to see if we’re being asked to render a gallery block, and if the gallery block has the “slideshow” CSS class.
  2. If we’re rendering a gallery slideshow block, then…
    1. Enqueue our frontend assets.
    2. Open containing div.
    3. For each image in the gallery…
      1. Determine the properties for this figure element.
      2. Opening the figure element.
      3. Render the img element.
      4. Close the figure element.
    4. Close the containing div.

That’s the PHP bit. It’s fairly standard program-flow. Check the inputs… run the core logic… return the result (in this case, $block_content is a string that contains our HTML.

Notice how we try to pick up on some other properties from the gallery, such as the image size, which is accessible via $block['attrs']['sizeSlug'].

We don’t do anything with the gallery’s image-click target, but it would be easy to extend the code so people could click on the images. If you do this, be careful to consider the user experience (UX). Trying to click on something that’s moving, fading or changing shape is difficult and annoying for most users.

WordPress Block : Slide Show Properties
WordPress Gallery Block

You should be able to set up a gallery now, and the page will load. It’ll only show the first slide because all the other slides have style="display:none;" set against them. Now we need to create the front-end assets.

CSS Styles & Transitions

Open wpt-gallery-to-slideshow/wptgts.css and paste the following into it:

/**
 * WP Tutorials : Gallery-to-Slideshow (WPTGTS)
 *
 * https://wp-tutorials.tech/refine-wordpress/turn-wordpress-gallery-into-a-slideshow/
 * 
 * Changelog
 * 
 * 2024-03-23 : Add "line-height:0;" to the .wptgts.slideshow selector to fix gallery
 *              height issues with some themes.
 */
.wptgts.slideshow {
	position: relative;
	line-height: 0;
}

.wptgts.slideshow figure {
	margin: 0;
	text-align: center;
	transition: 0.6s;
}

.wptgts.slideshow.slideshow-slow figure {
	transition: 1.2s;
}

.wptgts.slideshow.slideshow-fast figure {
	transition: 0.2s;
}

.wptgts.slideshow figure.current-slide {
	opacity: 1.0;
}

.wptgts.slideshow figure:not(.current-slide) {
	opacity: 0.0;
}

.wptgts.slideshow figure:not(:first-of-type) img {
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

Notice how we’ve got a couple of extra definitions:

  • slideshow-slow
  • slideshow-fast

These let us have a bit of control over the transition time – how long it takes to fade from one slide to the next. In the gallery block, set the CSS classes to “slideshow slideshow-slow” to get a fade-through time of 1.2 seconds.

The JavaScript Bit

We could write the JavaScript code in fewer lines if we used jQuery, but that means including more code in our page-load. To keep things really lean and efficient, we’ve only used core JavaScript… no external dependencies. The code should still be easy to follow.

Open wpt-gallery-to-slideshow/wptgts.js and paste the following into it:

/**
 * WP Tutorials : Gallery-to-Slideshow (WPTGTS)
 *
 * https://wp-tutorials.tech/refine-wordpress/turn-wordpress-gallery-into-a-slideshow/
 */
document.addEventListener('DOMContentLoaded', function() {
    'use strict';

    // Uncomment this to verify that the JS file has loaded properly.
    // console.log('WPT Gallery to Slideshow  : load');

    if (typeof wptgtsData != 'undefined') {
        // Uncomment this to verify that wp_localize_script has worked correctly.
        // console.log(`WPT Gallery to Slideshow  : init ${wptgtsData.selector}`);

        // Initialise our slideshow(s).
        document.querySelectorAll(wptgtsData.selector).forEach(function(slideshow) {
            nextSlide(slideshow);
        });

        function nextSlide(slideshow) {
            // The slideshow variable is a reference to the outer div element.

            var slideIndex = 0;
            var duration = parseInt(slideshow.dataset.wptgtsDuration);

            if (typeof slideshow.dataset.wptgtsSlideIndex == 'undefined') {
                slideshow.dataset.wptgtsSlideIndex = slideIndex;
                duration = parseInt(slideshow.dataset.wptgtsFirstDuration);

                // On the first iteration, remove the style attribute,
                // which holds "display:none;" for the initial page-load.
                slideshow.querySelectorAll('figure').forEach(function(slide) {
                    slide.removeAttribute('style');
                });
            } else {
                slideIndex = parseInt(slideshow.dataset.wptgtsSlideIndex);

                // Increment the slide index, and maybe reset back to zero if
                // we've gone past last slide.
                ++slideIndex;
                if (slideIndex >= parseInt(slideshow.dataset.wptgtsImageCount)) {
                    slideIndex = 0;
                }

                // Remove the current-slide class from any slides that have it.
                slideshow.querySelectorAll('.current-slide').forEach(function(slide) {
                    slide.classList.remove('current-slide');
                });

                // Add the current-slide class to the new slide.
                var newCurrentSlide = slideshow.querySelector(`figure:nth-of-type(${slideIndex+1})`);
                if (newCurrentSlide) {
                    newCurrentSlide.classList.add('current-slide');
                }

                // Save the current slide index against the slideshow.
                slideshow.dataset.wptgtsSlideIndex = slideIndex;
            }

            // Uncomment this to verify that slideIndex is incrementing and resetting properly.
            // console.log(`Slide: ${slideIndex+1} / ${slideshow.dataset.wptgtsImageCount} (dur=${duration})`);

            setTimeout(
                function() { nextSlide(slideshow) },
                duration
            );
        }
    }
});

The code breaks down like this:

  1. If there’s a global variable called wptgtsData, which is defined in wpt-gallery-to-slideshow.php by using wp_localize_script(), then…
    1. For each element that matches the selector in wptgtsData.selector
      1. Call nextSlide( slideshow )

The nextSlide( slideshow ) function…

  • If this is the first time the function has been called for this slideshow, then…
    • Set the slideshow’s current slide index to zero (the first slide).
    • Remove style=”display:none;” from all the figure elements.
  • else…
    • Get and increment this slideshow’s current slide index.
    • If the current slide index is higher than the number of slides, then…
      • Set the current slide index back to zero.
    • Remove the current-slide CSS class from all figure elements. It should only ever be on one element, but the code to remove it from a single element is almost the same as removing the class from all elements.
    • Select the nth figure element, where n is the current slide index…
      • Add current-slide to the figure element.
    • Store the current slide index against the slide show, so we can recall it next time around.
  • Set a timeout to call nextSlide() again in duration seconds.

infoBe very careful with your slide index in here. In code, sequences are usually zero-indexed, rather than one-indexed. This means the first item in a sequence is item zero. In CSS, we use :nth-of-type(n) to find the current slide, and this function is one-indexed. So we have to add one to our zero-indexed slide index.

Wrap Up and Test

Once everything’s saved, reload your content have a play with it. Don’t forget to set the CSS Class of your gallery to “slideshow”, and check that it works with different image sizes (medium, large, etc).

It should be easy to extend the code with things like image captions – you could inject figcaption elements within the slides. You could also look at setting up a click handler to pop the gallery into a responsive lightbox, perhaps by specifying CSS classes like “slideshow slideshow-slow slideshow-lightbox”? That would be neat.

Happy sliding and fading 😎 👍

Like This Tutorial?

Let us know

WordPress plugins for developers

2 thoughts on “Turn any WordPress Gallery into a Slide Show”

  1. Seriously cool!! I could never write this myself and it is exactly what I needed. Thank you for sharing this!!

    Reply

Leave a comment