search ]

How to Create Masonry with Loading Animations in WordPress

I recently wrote about Isotope and how to use this library to create a filter or post filtering in WordPress. As mentioned, Isotope has a young sibling called masonry.js, which allows you to create nice layouts and essentially enables creating grid-based layouts without filtering and sorting options.

Using masonry.js is ideal for creating portfolios of images, projects, and similar content. So, in this post, which borrows quite a bit of code from the following post by Mary Lou, I’ll show you how to create a portfolio using masonry.js and use anime.js to create cool loading animations for that portfolio.

The reason I’m writing this post is to make it easier for you to create such a portfolio and save you the trouble of digging into the code that Mary Lou wrote.

Additionally, it seems that in your project, you may choose only one effect and implement the animation during page load rather than on button click, as Mary demonstrates. I’ll show you how to do that later.

I’ll mention that I won’t go into the code details in this post, but I’ll show examples of effects you can use and explain how to implement those effects on Masonry (our portfolio).

Let’s start, but not before we see what we’re talking about and what animations can be achieved with the code provided in this post…

Loading Animations for Portfolios – Examples of Effects

Click the buttons below to see the loading effect and the options available to you:

If you’re interested in how I added Lightbox to images on click, take a look at the post on adding Lightbox to images and content using Lity.

Loading the Desired Assets for Portfolio and Animations

The first thing we will do is load the assets in your WordPress theme. Add the following code to your functions.php file in your child theme. Please note that there is a dependency between the files:

function masonry_assets() {

    wp_enqueue_script(
        'masonry',
        get_stylesheet_directory_uri() . '/js/masonry.pkgd.min.js',
        array('jquery'),
        '1.0.1',
        true
    );

    wp_enqueue_script(
        'images-loaded',
        get_stylesheet_directory_uri() . '/js/imagesloaded.pkgd.min.js',
        array('masonry'),
        '1.0.1',
        true
    );

    wp_enqueue_script(
        'anime',
        get_stylesheet_directory_uri() . '/js/anime.min.js',
        array('images-loaded'),
        '1.0.1',
        true
    );

    wp_enqueue_script(
        'masonry-init',
        get_stylesheet_directory_uri() . '/js/masonry-init.js',
        array('anime'),
        '1.0.1',
        true
    );

}

add_action('wp_enqueue_scripts', 'masonry_assets');

You can find all four files in the following link. Copy these files to the js directory in your child theme. To make it easier for you, here are the images we used, courtesy of unsplash.com.

The Portfolio Markup

Here’s the HTML for our portfolio:

<div class="grid grid--type-c">
	<div class="grid__sizer"></div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/1.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/2.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/3.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/4.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/5.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/6.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/10.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/11.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/7.jpg"/>
		</a>
	</div>
	<div class="grid__item">
		<a class="grid__link" href="#">
			<img class="grid__img no-lazy" src="/img/8.jpg"/>
		</a>
	</div>
</div>

Note that I added the no-lazy class to the images. I disabled lazy loading for images with this class since it interfered with displaying the portfolio correctly.

It’s important to specify the correct path for the images based on where you placed them.

The Portfolio Styling (CSS)

Here’s the CSS I used. I won’t go into detail, but you may need to adjust it to match your theme’s CSS:

.loading::before,
.loading::after {
    content: '';
    position: fixed;
    z-index: 1000;
}

.loading::before {
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: #2c2d31;
}

.loading::after {
    top: 50%;
    left: 50%;
    width: 40px;
    height: 40px;
    margin: -20px 0 0 -20px;
    border: 8px solid #383a41;
    border-bottom-color: #565963;
    border-radius: 50%;
    animation: animLoader 0.8s linear infinite forwards;
}


.control.control--effects {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    margin-bottom: 20px;
}

/* Grid */

.grid {
    position: relative;
    z-index: 2;
    display: block;
    margin: 0 auto;
    overflow: hidden;
    opacity: 0;
}

.grid a {
    -webkit-transition: none;
    transition: none;
}

.grid--hidden {
    position: fixed !important;
    z-index: 1;
    top: 0;
    left: 0;
    width: 100%;
    pointer-events: none;
    opacity: 0;
}

.grid--loading::before,
.grid--loading::after {
    content: '';
    z-index: 1000;
}

.grid--loading::before {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(255, 255, 255, 0.9);
}

.grid--loading::after {
    position: absolute;
    top: calc(25vh - -30px);
    left: 50%;
    width: 40px;
    height: 40px;
    margin: 0 0 0 -20px;
    border: 8px solid #383a41;
    border-bottom-color: #565963;
    border-radius: 50%;
    animation: animLoader 0.8s linear forwards infinite;
}

.grid__sizer {
    margin-bottom: 0 !important;
}

.grid__link,
.grid__img {
    display: block;
}

.grid__img {
    width: 100%;
    max-width: unset;
    height: unset;
}

.grid__deco {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
}

.grid__deco path {
    fill: none;
    stroke: #fff;
    stroke-width: 2px;
}

.grid__reveal {
    position: absolute;
    z-index: 50;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    opacity: 0;
    background-color: #2c2d31;
}

.grid .grid__item,
.grid .grid__sizer {
    width: calc(50% - 20px);
    margin: 0 10px 20px;
}

.grid.grid--type-c.active {
    opacity: 1;
}

@keyframes animLoader {
    to {
        transform: rotate(360deg);
    }
}


@keyframes octocat-wave {
    0%,
    100% {
        transform: rotate(0);
    }
    20%,
    60% {
        transform: rotate(-25deg);
    }
    40%,
    80% {
        transform: rotate(10deg);
    }
}


@media screen and (min-width: 70em) {


    .grid--type-c .grid__item,
    .grid--type-c .grid__sizer {
        width: calc(25% - 16px);
        margin: 0 8px 16px;
    }
}

Note that you should load this CSS after the CSS in your theme.

The masonry-init.js File and Implementing the Grid Loading Effect

In the masonry-init.js file, which we included earlier, the complete code responsible for each effect is available, as well as the option to implement the effect on page load instead of a button click.

It’s likely that you’ll want to choose only one effect for your project and make that effect work on page load instead of a button click. So here’s the code that accomplishes this for the “Hapi” effect:

/**
 * main.js
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 *
 * Copyright 2017, Codrops
 * http://www.codrops.com
 */
;(function (window) {

    /**
     * GridLoaderFx obj.
     */
    function GridLoaderFx(el, options) {
        this.el = el;
        this.items = this.el.querySelectorAll('.grid__item > .grid__link');
    }

    /**
     * Effects.
     */
    GridLoaderFx.prototype.effects = {
        'Hapi': {
            animeOpts: {
                duration: function (t, i) {
                    return 600 + i * 75;
                },
                easing: 'easeOutExpo',
                delay: function (t, i) {
                    return i * 50;
                },
                opacity: {
                    value: [0, 1],
                    easing: 'linear'
                },
                scale: [0, 1]
            }
        }
    };

    GridLoaderFx.prototype._render = function (effect) {
        // Reset styles.
        this._resetStyles();

        var self = this,
            effectSettings = this.effects[effect],
            animeOpts = effectSettings.animeOpts

        if (effectSettings.perspective != undefined) {
            [].slice.call(this.items).forEach(function (item) {
                item.parentNode.style.WebkitPerspective = item.parentNode.style.perspective = effectSettings.perspective + 'px';
            });
        }

        if (effectSettings.origin != undefined) {
            [].slice.call(this.items).forEach(function (item) {
                item.style.WebkitTransformOrigin = item.style.transformOrigin = effectSettings.origin;
            });
        }

        if (effectSettings.lineDrawing != undefined) {
            [].slice.call(this.items).forEach(function (item) {
                // Create SVG.
                var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                    path = document.createElementNS('http://www.w3.org/2000/svg', 'path'),
                    itemW = item.offsetWidth,
                    itemH = item.offsetHeight;

                svg.setAttribute('width', itemW + 'px');
                svg.setAttribute('height', itemH + 'px');
                svg.setAttribute('viewBox', '0 0 ' + itemW + ' ' + itemH);
                svg.setAttribute('class', 'grid__deco');
                path.setAttribute('d', 'M0,0 l' + itemW + ',0 0,' + itemH + ' -' + itemW + ',0 0,-' + itemH);
                path.setAttribute('stroke-dashoffset', anime.setDashoffset(path));
                svg.appendChild(path);
                item.parentNode.appendChild(svg);
            });

            var animeLineDrawingOpts = effectSettings.animeLineDrawingOpts;
            animeLineDrawingOpts.targets = this.el.querySelectorAll('.grid__deco > path');
            anime.remove(animeLineDrawingOpts.targets);
            anime(animeLineDrawingOpts);
        }

        if (effectSettings.revealer != undefined) {
            [].slice.call(this.items).forEach(function (item) {
                var revealer = document.createElement('div');
                revealer.className = 'grid__reveal';
                if (effectSettings.revealerOrigin != undefined) {
                    revealer.style.transformOrigin = effectSettings.revealerOrigin;
                }
                if (effectSettings.revealerColor != undefined) {
                    revealer.style.backgroundColor = effectSettings.revealerColor;
                }
                item.parentNode.appendChild(revealer);
            });

            var animeRevealerOpts = effectSettings.animeRevealerOpts;
            animeRevealerOpts.targets = this.el.querySelectorAll('.grid__reveal');
            animeRevealerOpts.begin = function (obj) {
                for (var i = 0, len = obj.animatables.length; i < len; ++i) {
                    obj.animatables[i].target.style.opacity = 1;
                }
            };
            anime.remove(animeRevealerOpts.targets);
            anime(animeRevealerOpts);
        }

        if (effectSettings.itemOverflowHidden) {
            [].slice.call(this.items).forEach(function (item) {
                item.parentNode.style.overflow = 'hidden';
            });
        }

        animeOpts.targets = effectSettings.sortTargetsFn && typeof effectSettings.sortTargetsFn === 'function' ? [].slice.call(this.items).sort(effectSettings.sortTargetsFn) : this items;
        anime.remove(animeOpts.targets);
        anime(animeOpts);
    };

    GridLoaderFx.prototype._resetStyles = function () {
        this.el.style.WebkitPerspective = this.el.style.perspective = 'none';
        [].slice.call(this items).forEach(function (item) {
            var gItem = item.parentNode;
            item.style.opacity = 0;
            item.style.WebkitTransformOrigin = item.style.transformOrigin = '50% 50%';
            item.style.transform = 'none';

            var svg = item.parentNode.querySelector('svg.grid__deco');
            if (svg) {
                gItem.removeChild(svg);
            }

            var revealer = item.parentNode.querySelector('.grid__reveal');
            if (revealer) {
                gItem.removeChild(revealer);
            }

            gItem.style.overflow = '';
        });
    };

    window.GridLoaderFx = GridLoaderFx;

    var body = document.body,
        grids = [].slice.call(document.querySelectorAll('.grid')), masonry = [],
        currentGrid = 0,
        // The GridLoaderFx instances.
        loaders = [],
        loadingTimeout;


    function init() {
        // Preload images
        imagesLoaded(body, function () {

            // Initialize Masonry on each grid.
            grids.forEach(function (grid) {
                var m = new Masonry(grid, {
                    itemSelector: '.grid__item',
                    columnWidth: '.grid__sizer',
                    percentPosition: true,
                    transitionDuration: 0,
                    originLeft: true
                });
                masonry.push(m);
                // Hide the grid.
                // Init GridLoaderFx.
                loaders.push(new GridLoaderFx(grid));
            });

            // Remove loading class from body
            body.classList.remove('loading');

            $('.grid').addClass('active');

            loaders[currentGrid]._render('Hapi');
        });
    }

    init();

})(window);

Hapi is the only effect in this code, but of course, you can replace it with any other effect by changing lines 25-40 with the desired effect from the file that I attached previously.

Also, in this code, I removed the option to change the effect by clicking on those buttons and made sure to implement the effect when the page is loaded (line 177). If you replaced the effect with another, you should also change its name in this line accordingly.

You can add support for right-to-left (RTL) websites by using the parameter originLeft: true on line number 165.

I hope this post helps you. Questions and comments are welcome… 🙂

2 Comments...

Leave a Comment

To add code, use the buttons below. For instance, click the PHP button to insert PHP code within the shortcode. If you notice any typos, please let us know!

Savvy WordPress Development