Create smooth page transitions using Barba.js w/ WordPress, Elementor & Gravity Forms.

It has been my life-long mission (or at least the last few months) to get smooth page transitions working with WordPress themes and plugins like Elementor and Gravity Forms. I finally found a working solution using Barba.js 🥳 which also plays nice with caching plugins like WP Rocket and Litespeed Cache. There isn’t alot of information around about this so if you’re on a journey to get this working with your WordPress stack, hopefully this helps.

Why use page transitions?

Firstly why not? But also for the following reasons:

  • Enhance the user flow and experience
  • Minimize browser HTTP requests
  • Make your website run like a SPA (Single Page Application) 
  • Reduce the delay between your pages
  • It’s badass 😎

What we will be using in this guide:

You can follow the links above to install these either via CDN or NPM.

If you’re looking for a simple working example of Barba page transitions using GSAP, I recommend checking this Codepen example we put together.

Let’s get started

This solution works by swapping the classes in the body element and then re-loading the necessary assets during each page transition.

First you should edit your single.php, index.php and any other templates you use have a similar structure to the following:

<body data-barba="wrapper"> <!-- put here content that will not change between your pages, like <header> or <nav> --> <main data-barba="container" data-barba-namespace="home"> <!-- put here the content you wish to change between your pages, like your main content <h1> or <p> --> </main> <!-- put here content that will not change between your pages, like <footer> --> </body>

Next you will need to create a JS file titled “page-transitions.js” or whatever you prefer and either enqueue this in your functions.php file or compile it into your theme using something like webpack.

This is what your page-transitions.js file should look like:

// This function helps add and remove js and css files during a page transition function loadjscssfile(filename, filetype) { if (filetype == "js") { //if filename is a external JavaScript file const existingScript = document.querySelector('script[src="${filename}"]'); if (existingScript) { existingScript.remove(); } var fileref = document.createElement("script"); fileref.setAttribute("type", "text/javascript"); fileref.setAttribute("src", filename); } else if (filetype == "css") { //if filename is an external CSS file const existingCSS = document.querySelector(`link[href='${filename}']`); if (existingCSS) { existingCSS.remove(); } var fileref = document.createElement("link"); fileref.setAttribute("rel", "stylesheet"); fileref.setAttribute("type", "text/css"); fileref.setAttribute("href", filename); } if (typeof fileref != "undefined") document.getElementsByTagName("head")[0].appendChild(fileref); } barba.hooks.beforeEnter(({ current, next }) => { // Set <body> classes for the 'next' page if (current.container) { // // only run during a page transition - not initial load let nextHtml = next.html; let response = nextHtml.replace( /(<\/?)body( .+?)?>/gi, "$1notbody$2>", nextHtml ); let bodyClasses = $(response).filter("notbody").attr("class"); $("body").attr("class", bodyClasses); // ELEMENTOR // Where the magic happens - this loads the new Elementor styles and removes the old styles if (bodyClasses.includes("elementor-page")) { let currentPageId = current.container .querySelector(".elementor") .getAttribute("data-elementor-id"); let nextPageId = next.container .querySelector(".elementor") .getAttribute("data-elementor-id"); let oldScriptURL = "/wp-content/uploads/elementor/css/post-" + currentPageId + ".css"; let newScriptURL = "/wp-content/uploads/elementor/css/post-" + nextPageId + ".css"; // Add this for cache fix: ?cachebuster=' + new Date().getTime() const oldElementorScript = document.querySelector( 'link[src="' + oldScriptURL + '"]' ); if (oldElementorScript) { oldElementorScript.remove(); } loadjscssfile(newScriptURL, "css"); } } // GRAVITY FORMS const baseURL = window.location.hostname; const protocol = window.location.protocol; // Here we are manually reloading the gravity form scripts and styles. If you find that you get errors in future with missing assets, simply add them below. const gravityFormJS = "/wp-content/plugins/gravityforms/js/gravityforms.min.js"; const gravityFormsJquery = "/wp-content/plugins/gravityforms/js/jquery.json.min.js"; const gformReset = "/wp-content/plugins/gravityforms/css/formreset.min.css"; const gformMainCSS = "/wp-content/plugins/gravityforms/css/formsmain.min.css"; const gformReadyclass = "/wp-content/plugins/gravityforms/css/readyclass.min.css"; const gformBrowser = "/wp-content/plugins/gravityforms/css/browsers.min.css"; const gformVariables = 'var gf_global = {"gf_currency_config":{"name":"Australian Dollar","symbol_left":"$","symbol_right":"","symbol_padding":" ","thousand_separator":",","decimal_separator":".","decimals":2},"base_url":"' + protocol + "//" + baseURL + '/wp-content/plugins/gravityforms","number_formats":[],"spinnerUrl":"' + protocol + "//" + baseURL + '/wp-content/plugins/gravityforms/images/spinner.gif"};'; const gformWrapper = next.container.querySelectorAll(".gform_wrapper"); // const gformSomethingElse = '/wp-content/plugins/gravityforms/css/somethingElse.min.css'; if (gformWrapper) { // run if the page contains a form const gformVariablesScript = document.createElement("script"); gformVariablesScript.innerHTML = gformVariables; document.body.appendChild(gformVariablesScript); loadjscssfile(gravityFormJS, "js"); loadjscssfile(gravityFormsJquery, "js"); loadjscssfile(gformReset, "css"); loadjscssfile(gformMainCSS, "css"); loadjscssfile(gformReadyclass, "css"); loadjscssfile(gformBrowser, "css"); // loadjscssfile(gformSomethingElse, 'css') if (window["gformInitDatepicker"]) { gformInitDatepicker(); } gformWrapper.forEach((element) => { const parent = element.parentElement; const scripts = parent.querySelectorAll("script"); scripts.forEach((script) => { const scriptCode = script.innerHTML; const newScript = document.createElement("script"); script.remove(); newScript.innerHTML = scriptCode; parent.appendChild(newScript); }); }); // ALLOW ELEMENTOR VIDEOS TO AUTOPLAY AFTER TRANSITION let elementorVideos = document.querySelectorAll(".elementor-video"); if (typeof elementorVideos != "undefined" && elementorVideos != null) { elementorVideos.forEach((video) => { video.play(); }); } if (current.container) { // only run during a page transition - not initial load // add any custom JS here that you would like to load on each page elementorFrontend.init(); } } }); const tl = gsap.timeline({ defaults: { ease: "power3.inOut" } }); function leaveAnimation(e) { return new Promise(async (resolve) => { await tl .to(e, { duration: 1.5, y: -100, opacity: 0, }) // add any animation you like here .then(); resolve(); setTimeout(function () { e.style.opacity = "0"; }, 4000); }); } function enterAnimation(e) { return new Promise(async (resolve) => { await tl .from(e, { duration: 1.5, y: 100, opacity: 0, }) // add any animation you like here .then(); resolve(); }); } barba.init({ timeout: 5000, debug: false, // turn this to "true" to debug transitions: [ { sync: false, leave: ({ current }) => leaveAnimation(current.container), enter: ({ next }) => enterAnimation(next.container), }, ], });

Boom! That’s it 🥳 You will just need to play around with the GSAP animations above to something more custom that works for you.

Disclaimer: This is what is working for us but there may be times where you are using additional features from Gravity Forms, Elementor or other plugins. You should always check for any errors when setting this up to make sure no JS or CSS files are missing after a page transition.

You can simply copy any missing script or link url similar to the example “gformSomethingElse”.

WP Rocket configuration

We ran into some minor issues with WP Rocket and Gravity Forms. I’d recommend disabling “Combing Javascript Files” or exclude the these JS files in the WP Rocket settings.

Bonus: How to get Elementor / Barba.js working with Sage?

If you use the WordPress Starter Theme Sage like we do, here is how we configured it to take advantage of routes:

app.blade.js file

<html {!! get_language_attributes() !!}> <!-- ... --> <body @php body_class() @endphp data-barba="wrapper"> <!-- ... --> <div class="wrap container" role="document" data-barba="container"> <!-- ... --> </div> <!-- ... --> </body> </html>

main.js file

// import external dependencies import 'jquery'; import 'gsap'; import barbaInit from './page-transitions'; // Import everything from autoload import './autoload/**/*' // import local dependencies import Router from './util/Router'; import common from './routes/common'; import home from './routes/home'; import aboutUs from './routes/about'; /** Populate Router instance with DOM routes */ const routes = new Router({ // All pages common, // Home page home, // About Us page, note the change from about-us to aboutUs. aboutUs, }); // Load Events jQuery(document).ready(() => { routes.loadEvents(); barbaInit(routes); });

page-transition.js

import barba from "@barba/core"; import { gsap } from "gsap"; export default function (routes) { function loadjscssfile(filename, filetype) { if (filetype == "js") { //if filename is a external JavaScript file const existingScript = document.querySelector( `script[src="${filename}"]` ); if (existingScript) { existingScript.remove(); } var fileref = document.createElement("script"); fileref.setAttribute("type", "text/javascript"); fileref.setAttribute("src", filename); } else if (filetype == "css") { //if filename is an external CSS file const existingCSS = document.querySelector(`link[href='${filename}']`); if (existingCSS) { existingCSS.remove(); } var fileref = document.createElement("link"); fileref.setAttribute("rel", "stylesheet"); fileref.setAttribute("type", "text/css"); fileref.setAttribute("href", filename); } if (typeof fileref != "undefined") document.getElementsByTagName("head")[0].appendChild(fileref); } barba.hooks.beforeEnter(({ current, next }) => { // Set <body> classes for 'next' page if (current.container) { let nextHtml = next.html; let response = nextHtml.replace( /(<\/?)body( .+?)?>/gi, "$1notbody$2>", nextHtml ); let bodyClasses = $(response).filter("notbody").attr("class"); $("body").attr("class", bodyClasses); // ELEMENTOR if (bodyClasses.includes("elementor-page")) { let currentPageId = current.container .querySelector(".elementor") .getAttribute("data-elementor-id"); let nextPageId = next.container .querySelector(".elementor") .getAttribute("data-elementor-id"); let oldScriptURL = "/wp-content/uploads/elementor/css/post-" + currentPageId + ".css"; let newScriptURL = "/wp-content/uploads/elementor/css/post-" + nextPageId + ".css"; // Add this for cache fix: ?cachebuster=' + new Date().getTime() const oldElementorScript = document.querySelector( 'link[src="' + oldScriptURL + '"]' ); if (oldElementorScript) { oldElementorScript.remove(); } loadjscssfile(newScriptURL, "css"); } } // GRAVITY FORMS const baseURL = window.location.hostname; const protocol = window.location.protocol; const gravityFormJS = "/wp-content/plugins/gravityforms/js/gravityforms.min.js"; const gravityFormsJquery = "/wp-content/plugins/gravityforms/js/jquery.json.min.js"; const gformReset = "/wp-content/plugins/gravityforms/css/formreset.min.css"; const gformMainCSS = "/wp-content/plugins/gravityforms/css/formsmain.min.css"; const gformReadyclass = "/wp-content/plugins/gravityforms/css/readyclass.min.css"; const gformBrowser = "/wp-content/plugins/gravityforms/css/browsers.min.css"; const gformVariables = 'var gf_global = {"gf_currency_config":{"name":"Australian Dollar","symbol_left":"$","symbol_right":"","symbol_padding":" ","thousand_separator":",","decimal_separator":".","decimals":2},"base_url":"' + protocol + "//" + baseURL + '/wp-content/plugins/gravityforms","number_formats":[],"spinnerUrl":"http://birdpress.local/wp-content/plugins/gravityforms/images/spinner.gif"};'; const gformWrapper = next.container.querySelectorAll(".gform_wrapper"); if (gformWrapper) { const gformVariablesScript = document.createElement("script"); gformVariablesScript.innerHTML = gformVariables; document.body.appendChild(gformVariablesScript); loadjscssfile(gravityFormJS, "js"); loadjscssfile(gravityFormsJquery, "js"); loadjscssfile(gformReset, "css"); loadjscssfile(gformMainCSS, "css"); loadjscssfile(gformReadyclass, "css"); loadjscssfile(gformBrowser, "css"); if (window["gformInitDatepicker"]) { gformInitDatepicker(); } gformWrapper.forEach((element) => { const parent = element.parentElement; const scripts = parent.querySelectorAll("script"); scripts.forEach((script) => { const scriptCode = script.innerHTML; const newScript = document.createElement("script"); script.remove(); newScript.innerHTML = scriptCode; parent.appendChild(newScript); }); }); // PLAY VIDEOS AFTER TRANSITION let elementorVideos = document.querySelectorAll(".elementor-video"); if (typeof elementorVideos != "undefined" && elementorVideos != null) { elementorVideos.forEach((video) => { video.play(); }); } if (current.container) { // if transitioning from a current page elementorFrontend.init(); routes.loadEvents(); } } }); const tl = gsap.timeline({ defaults: { ease: "power3.inOut" } }); function leaveAnimation(e) { return new Promise(async (resolve) => { await tl .to(".transition-block", { duration: 1.5, yPercent: -100, }) .to(e, { duration: 1.5, y: -100, opacity: 0, delay: -1.5, }) .then(); resolve(); setTimeout(function () { e.style.opacity = "0"; }, 4000); }); } function enterAnimation(e) { return new Promise(async (resolve) => { await tl .to(".transition-block", { duration: 1.5, yPercent: -200, }) .from(e, { duration: 1.5, y: 100, opacity: 0, delay: -1.5, }) .set(".transition-block", { yPercent: 0 }) .then(); resolve(); }); } barba.init({ timeout: 5000, debug: false, transitions: [ { sync: false, leave: ({ current }) => leaveAnimation(current.container), enter: ({ next }) => enterAnimation(next.container), }, ], }); }