I’ve had a lot of issues building swipers / carousels in WordPress that my clients can edit on their own in a way that makes sense. I’ve used plugins that allowed them to build the sliders on another page and then include a shortcode in the content, but shortcodes are a hard concept for the clients to grasp. I also used Advanced Custom Fields with the Repeater add-on (or now ACF Pro), but that only allowed me to put one carousel on the page, the fields appeared at the bottom of the editor below all of the content, there was no way inject a carousel in the middle of content, and adding the ability to edit options such as delay, style, etc… was tricky.

Now the Gutenberg is here, we have the ability to create custom blocks that helped me solve this problem! Instead of using shortcodes which require the user / client to know what parameters to pass, we can create custom blocks to give the client a nice GUI to select options, add / remove slides, etc…

I created the block as a plugin using Create Guten Block, which is a nice starter project for building WordPress Gutenberg block plugins. I did some research on vanilla JavaScript carousel’s / sliders to find a solution that offered a lot of configuration options that was also lightweight and battle tested. I ended up using an open source slider library called Swiper as it offered many different options and settings in their API. They are also used by many big corporations and it’s also the swiper used in the Ionic Framework.

Whether you know how WordPress and Gutenberg blocks (React) work together or not, the setup is pretty straightforward. The edit function allows the user to add / remove images from the editor and then we save those images in the HTML format that Swiper requires in their API. I also added some basic options for the user to set speed, transition effect, loop, etc… I wrote some custom JavaScript that will then pick up the options from data attributes when Swiper initializes.

I don’t want to go into great detail on how Gutenberg blocks, so I’ve only included a small snippet of the edit code below:

edit: function(props) {
    const { images, autoplay, loop, speed, delay, effect } = props.attributes;

    // FULL CODE ON GITHUB
    // https://github.com/mread1208/mcr-gutenberg-swiper-carousel
    if (images.length > 0) {
        return [
            <inspectorcontrols>
                <panelbody title="{__(&quot;Carousel" settings")}="">
                    <panelrow>
                        <radiocontrol label="Auto Play" selected="{autoplay}" options="{[" {="" label:="" "true",="" value:="" "true"="" },="" "false",="" "false"="" }="" ]}="" onchange="{option" ==""> {
                                updateSliderSetting({ autoplay: option });
                            }}
                        /&gt;
                    </radiocontrol></panelrow>
                    <panelrow>
                        <textcontrol label="Delay" value="{delay}" onchange="{option" ==""> {
                                updateSliderSetting({ delay: option });
                            }}
                        /&gt;
                    </textcontrol></panelrow>
                    <panelrow>
                        <textcontrol label="Speed" value="{speed}" onchange="{option" ==""> {
                                updateSliderSetting({ speed: option });
                            }}
                        /&gt;
                    </textcontrol></panelrow>
                    <panelrow>
                        <radiocontrol label="Loop" selected="{loop}" options="{[" {="" label:="" "true",="" value:="" "true"="" },="" "false",="" "false"="" }="" ]}="" onchange="{option" ==""> {
                                updateSliderSetting({ loop: option });
                            }}
                        /&gt;
                    </radiocontrol></panelrow>
                    <panelrow>
                        <selectcontrol label="Effect" selected="{effect}" options="{[" {="" label:="" "slide",="" value:="" "slide"="" },="" "fade",="" "fade"="" "cube",="" "cube"="" "coverflow",="" "coverflow"="" "flip",="" "flip"="" }="" ]}="" onchange="{option" ==""> {
                                updateSliderSetting({ effect: option });
                            }}
                        /&gt;
                    </selectcontrol></panelrow>
                </panelbody>
            </inspectorcontrols>,
            <fragment>
                {images.map((img, imgMapIndex) =&gt; {
                    return [
                        <div class="media-row mcr-media-row">
                            <mediauploadcheck>
                                <mediaupload onselect="{selectedImg" =="">
                                        onSelectImage(selectedImg, images, imgMapIndex)
                                    }
                                    type="image"
                                    value={img.imgid}
                                    accept="image/*"
                                    type="image"
                                    className=""
                                    render={({ open }) =&gt; (
                                        <button classname="{&quot;image-button&quot;}" onclick="{open}">
                                            <img src="{img.url}">
                                        </button>
                                    )}
                                /&gt;
                                <div classname="mcr-media-row--delete-button">
                                    <button classname="{&quot;button" button-large"}="" onclick="{()" ==""> {
                                            removeImage(img, images);
                                        }}
                                    &gt;
                                        X
                                    </button>
                                </div>
                                <div classname="mcr-media-row--add-button">
                                    <mediaupload onselect="{selectedImage" =="">
                                            addImage(selectedImage, images, imgMapIndex)
                                        }
                                        type="image"
                                        accept="image/*"
                                        type="image"
                                        render={({ open }) =&gt; (
                                            <button classname="{&quot;button" button-large"}="" onclick="{open}">
                                                Add Image
                                            </button>
                                        )}
                                    /&gt;
                                </mediaupload></div>
                            </mediaupload></mediauploadcheck>
                        </div>
                    ];
                })}
            </fragment>
        ];
    } else {
        return (
            <fragment>
                <div classname="{props.className}">
                    <mediaplaceholder icon="format-gallery" classname="{props.className}" labels="{{" title:="" __("carousel"),="" name:="" __("images")="" }}="" onselect="{onSelectImages}" accept="image/*" type="image" multiple="">
                </mediaplaceholder></div>
            </fragment>
        );
    }
}

We first check our image prop to see if there are any images currently saved. When the user first drops the block on the page, there obliviously won’t be any images so we just show them the media button to add images.

Once the user adds images, we then need to display all of those images on the editor as well as display editable block options so that the user can customize how the slider functions. The first part of this code you can see that I’m using the WordPress InspectorControls component to add options for transition, loop, effect, etc… These are the options that appear in the sidebar of the block component. The next part of the code loops over all of the images and add them as buttons so the edit image screen is displayed when the user clicks on each image. I’m also showing a delete button so the user can delete images 1 by 1.

The next snippet of code is the “Save” function which is what is ultimately rendered on the front end of the site.

save: function(props) {
    const { images, autoplay, loop, speed, delay, effect } = props.attributes;
    return (
        <div
            className={`swiper-container mcr-swiper-container js-mcr-swiper-container`}
            data-mcr-carousel-autoplay={autoplay}
            data-mcr-carousel-delay={delay}
            data-mcr-carousel-loop={loop}
            data-mcr-carousel-speed={speed}
            data-mcr-carousel-effect={effect}
        >
            <div className="swiper-wrapper mcr-swiper-wrapper">
                {images.map((image, index) => {
                    return (
                        <div className="swiper-slide mcr-swiper-slide">
                            <img src={image.url} alt={image.alt} />
                        </div>
                    );
                })}
            </div>
            <div className="swiper-pagination js-mcr-swiper-pagination" />
            <div className="js-mcr-swiper-button-prev swiper-button-prev mcr-swiper-button-prev" />
            <div className="js-mcr-swiper-button-next swiper-button-next mcr-swiper-button-next" />
        </div>
    );
}

The HTML structure is formatted for Swiper to initialize and render the slider properly, but the data-attributes are custom to this plugin and allow me to pass slider attributes from the backend to the front-end. When you initialize a new Swiper slider, you can pass it override options to change the default loop, speed, transition, etc… on the slider. What I ended up doing was writing custom JS for the front-end to search the page for all classes of js-mcr-swiper-container. Then I loop through all of those elements, grab all of the data attributes, and initialize each Slider using the options from those data attributes:

var swiper = (function() {
    var swiperSliders = document.getElementsByClassName(
        "js-mcr-swiper-container"
    );
    for (i = 0; i < swiperSliders.length; i++) {
        var swiperOptions = {
            navigation: {
                nextEl: ".js-mcr-swiper-button-next",
                prevEl: ".js-mcr-swiper-button-prev"
            },
            pagination: {
                el: ".js-mcr-swiper-pagination"
            },
            speed: 500,
            slidesPerView: 1,
            autoplay: {
                delay: 4000
            }
        };
        // Set the options if the data attribute has been defined
        // We can add more as we need them: http://idangero.us/swiper/api/
        var spaceBetween = swiperSliders[i].getAttribute("data-space-between");
        if (spaceBetween != null) {
            swiperOptions["spaceBetween"] = parseFloat(spaceBetween);
        }
        var speed = swiperSliders[i].getAttribute("data-mcr-carousel-speed");
        if (speed != null) {
            swiperOptions["speed"] = parseFloat(speed);
        }
        var autoplay = swiperSliders[i].getAttribute(
            "data-mcr-carousel-autoplay"
        );
        if (autoplay != null) {
            var autoplayValue = autoplay === "true" ? true : false;
            var delay = swiperSliders[i].getAttribute("data-mcr-carousel-delay")
                ? parseFloat(swiperSliders[i].getAttribute("data-mcr-carousel-delay"))
                : "4000";
            if (autoplayValue) {
                swiperOptions["autoplay"] = {
                    delay: delay
                };
            } else {
                swiperOptions["autoplay"] = false;
            }
        }
        var loop = swiperSliders[i].getAttribute("data-mcr-carousel-loop");
        if (loop != null) {
            swiperOptions["loop"] = loop === "true" ? true : false;
        }
        var effect = swiperSliders[i].getAttribute("data-mcr-carousel-effect");
        if (effect != null) {
            if (effect === "fade") {
                swiperOptions["fadeEffect"] = {
                    crossFade: true
                };
            }
            if (effect === "cube") {
                swiperOptions["cubeEffect"] = {
                    slideShadows: false
                };
            }
            if (effect === "coverflow") {
                swiperOptions["coverflowEffect"] = {
                    rotate: 30,
                    slideShadows: false
                };
            }
            if (effect === "flip") {
                swiperOptions["flipEffect"] = {
                    rotate: 30,
                    slideShadows: false
                };
            }
        }
        new Swiper(swiperSliders[i], swiperOptions);

        console.log(swiperOptions);
    }
})();

This is a very high overview, but it hopefully explains how to create a basic Gutenberg block and how to pass attributes from the backend to the frontend using data attributes. You can download and view the entire plugin on Github here: https://github.com/mread1208/mcr-gutenberg-swiper-carousel