Skip to content

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>
Code language: HTML, XML (xml)

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),
    },
  ],
});
Code language: JavaScript (javascript)

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>
Code language: HTML, XML (xml)

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);
});

Code language: JavaScript (javascript)

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),
      },
    ],
  });
}

Code language: JavaScript (javascript)