Using Barba.js with Sage 9

Barba.js uses pushState and other AJAX techniques to load pages asynchronously while maintaining a sensible and useful browser history. By combining it with a JavaScript animation library, we can create a memorable browsing experience.

I used Sage and Barba while coding my freelance portfolio and the website of the graphic collective I’m part of.

This guide attempts to describe an efficient and useful way to integrate Barba with the Sage starter theme.

Note: This guide uses Sage v9.0.9 & Barba v2.9.7.

Table of Contents

Barba Hello, world!

Our Hello, world! is more metaphorical, as it will not display the familiar phrase but will instead simply add Barba to our website.

1 – Install Barba

$ yarn add @barba/core

2 – Define Wrapper and Container

wrapper, container and namespace

wrapper and container are concepts with special meaning to Barba.

The wrapper wraps the container.
Content inside the wrapper but outside the container will remain the same as the user browses your site.
Content inside the container will change;
it will be removed and loaded dynamically as links are clicked.

We will specify these sections using data-* attributes in our main template file.

<!-- app.blade.php -->

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

Now Barba will know what parts to load dynamically once it has been initialized.

3 – Add Barba to Our JS

For now we will be putting Barba in common.js so that it runs on every page.
We will examine how to optimize our code structure later.

// common.js

import barba from '@barba/core';

export default {
  init() {
    barba.init();
  },
  finalize() {
  },
};

Barba should now be working on your site! Clicking a link should load the page in question and change the URL, but won’t cause the browser to reload the page.

It’s not working!

  • Make sure your data-* attributes were correctly added to your template.
  • Make sure Barba is installed in your_theme/node_modules/@barba/core.
  • Make sure Barba is being initialized in your JavaScript.
  • Take a break and come back later! 😁

Animate the Transition

Now that Barba is up and running, let’s try to implement a simple fade-out/fade-in effect for each container load (i.e. each new page).

There are a number of mature JavaScript animation libraries.
I personally really like anime.js by Julian Garnier, but in the interest of remaining consistent with the official Barba documentation we will use GSAP in this guide.

The GSAP library is frequently use for web animation, but unfortunately it is not open source.

Start by installing the library:

$ yarn add gsap

Then import it:

// common.js

import barba from '@barba/core';
import gsap from 'gsap';

export default {
  init() {
    barba.init({
       /* ... */ 
    });
  },
  finalize() {
  },
};

First Try

Start by adding a basic transition that will be used for all pages.
(Later we’ll examine how to define different transitions for different pages).

We will specify the enter and leave functions, which define what will happen when the current container is removed (leave) and when the next container is loaded (enter).

We can access the current container with

data.current.container

and next page’s container with

data.next.container

We will implement a fade-out on the current container followed by a fade-in on the next container using the GSAP functions .to() and .from().
Note that the second argument is the duration of the animation in seconds (GSAP .to() documentation).

// common.js
// ...

barba.init({
  transitions: [
    {
      name: 'basic',
      leave: function (data) {
        gsap.to(data.current.container, 1, {opacity: 0,});
      },
      enter: function (data) {
        gsap.from(data.next.container, 1, {opacity: 0,});
      },
    },
  ],
});

I tried it and the fade-in works properly but the first container disappears immediately instead of fading out!

That’s correct!
leave and enter are executed at the same time, so we don’t see the leave animation.

Second Try

Barba offers many ways to implement synchronicity/asynchronicity, but to keep things simple we’ll only investigate one method here.
See the Barba documentation for the other options.

The use of this.async() tells Barba to wait for the GSAP callback before calling the enter function.

// common.js
// ...

barba.init({
  transitions: [
    {
      name: 'basic',
      leave: function (data) {
        gsap.to(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
      },
      enter: function (data) {
        gsap.from(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
      },
    },
  ],
});

I’m getting closer, but it still doesn’t work! The first container seems to sit on top of the other one before being removed.

That’s correct.
The first container is only removed when the entire transition is complete.
Until then it remains on the page…unless we find a way to remove it!

Last Try

You can access the parentNode of the container to remove it like so :

// common.js
// ...

barba.init({
      transitions: [
        {
          name: 'basic',
          leave: function (data) {
            gsap.to(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
          },
          enter: function (data) {
            // Remove the old container
            data.current.container.parentNode.removeChild(data.current.container);
            gsap.from(data.current.container, 1, {opacity: 0, onComplete: this.async(),});
          },
        },
      ],
});

Note:
We’re encountering this problem because we chose to use this.async() for our transitions.
Once the problem is solved, however, this method is much cleaner and leaves room for future implementation of much more complex transitions.

Perfect!

Namespaces

If you’ve decided to use Barba, you probably want something fancier than just fading in and out as you transition between pages.

Well, you’re in luck: Barba can help you with that!

1 – Adding Namespaces in Templates

Barba’s namespaces will allow you to easily use the same behavior on groups of pages.

For our purposes here, let’s use the current post_name as our namespace.
(In other situations you might make this logic more complicated to take into account things like post type, categories, etc.)

<!-- app.blade.php -->

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

2 – Using Namespaces in JavaScript

Now that you’ve defined namespaces, you can describe transitions for…

  • Going to a specific page.
  • Coming from a specific page.
  • Going to a specific page from a different one.

Note: These terms have nothing to with GSAP’s .to() and .from() functions!

Here is a simple implementation:

// common.js
// ...

barba.init({
      transitions: [
        {
          name: 'basic',
          /* ... */
        } , {
          name: 'to-some-page',
          to: {
            namespace: ['some-page'],
          },
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            alert('this is some page');
            /* ... */
          },
        },
      ],
});

I called my transition to-some-page, but the name of the transition doesn’t matter;
only the namespaces are taken into account.
Also note that namespace takes an array as an argument so you can specify multiple namespaces at once.

Load Page-Specific JavaScript

Here is where things begin to get a little more complicated.

So far we have:

  • Set up Barba on our site.
  • Set up some animations.
  • Set up multiple transitions for different namespaces.

But perhaps you noticed something:
Because Barba only loads content inside of its containers, page-specific JavaScript located after the <footer> and isn’t loaded or executed after a transition.

We’ll see what we can do about that in a moment, but first let’s take a look at Sage’s JavaScript routing system.

Sage JavaScript Routing System

How It Works

Sage implements a simple routing system for JavaScript files which boils down to a two-step process:

  1. Select what route will be called based on the WordPress <body> classes.
  2. Execute route events in a specific order, which is:
    • common init
    • page-specific init
    • page-specific finalize
    • common finalize

This system also allows us to easily split our JavaScript into several page-specific files.

To learn more about how this system works, open main.js and router.js in your Sage theme:
The router is straightforward and well-documented.

How to Create a Route

While not directly relevant to a Barba implementation, this knowledge is tangentially relevant, so here it is:

  • Create a new file for your route to live in: routes/somePage.js

  • Add the relevant boilerplate and any code you wish to run on that page:

    // somepage.js
    
    export default {
      init() {
        // JavaScript to be fired on the page
      },
    };
    
  • Add the route to main.js:

    // main.js
    // ...
    
    import somePage from './routes/somePage';
    /* ... */
    const routes = new Router({
      /* ... */
      somePage,
    });
    

Remember that in order for this route to be recognized and your code executed, your <body> must have the class somePage (or some-page).
You will likely encounter issues with <body> classes not updating once Barba is working, so be sure to read the fix for that here.

Now, back to our original problem:
How do we connect this system to Barba?

Using Routes With Barba

Right now our problem with Barba is that our JavaScript doesn’t load when we change pages.
The behavior we want is the following:

  • When we enter a page that was not loaded by Barba (i.e. we typed the URL manually, or Barba has been disabled):
    • Run common JS code for the whole page (container included).
    • Run page-specific JS code.
  • When we enter a page that was loaded by Barba:
    • Run common JS code for the container only.
    • Run page-specific JS code.

Note:
This assumes that page-specific JavaScript is only meant to affect the contents of the container.

The first thing we’ll do is split our common.js code in two parts:

  • "Global" JS outside the containers (header, menus, footer, etc…) will go into init().
  • JS inside the containers that must be loaded for each page will go into containerInit().

containerInit() will then be called:

  1. In the initial init() when Barba is loaded for the first time.
  2. Whenever Barba’s enter() function is fired.

Don’t worry:
Both calls won’t ever happen simultaneously.

// common.js

import barba from '@barba/core';

export default {
  containerInit() {
    // common code for all containers
    /* ... */
  },
  init() {
    // common code outside containers (header, menu, footer, etc.)
    /* ... */
    
    // container init
    this.containerInit();

    barba.init({
      transitions: [
        {
          name: 'basic',
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            // Load common code for all containers
            this.containerInit();

            /* ... */
          },
        },
      ],
    });  },
  finalize() {
  },
};

This will load the container-specific JS code common to all pages while still keeping the default behaviour if we access the page directly (via url) or if Barba is disabled.

If we want to add page-specific code, we can just import it and load it in the enter function for this page.

// common.js

import barba from '@barba/core';
import somePage from "./somePage";

export default {
  containerInit() {
    /* ... */
  },
  init() {
    this.containerInit();

    barba.init({
      transitions: [
        {
          name: 'basic',
            /* ... */
        } , {
          name: 'to-some-page',
          to: {
            container: ['some-page'],
          },
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            // Load this page JS
            somePage.init();

            // Load common code for all containers
            this.containerInit();

            /* ... */
          },
        },
      ],
    });  },
  finalize() {
  },
};

This works!
But it doesn’t feel very clean does it?

We’ll have to import all our pages into common.js, removing the semantic separation between routes.
common.js will also end up full of Barba settings and configuration–code that seems like it should be in a separate file…

So let’s do it!

Organize Your Code

If we want a single file with all of our Barba code, then we’ll have to import all of our routes in order to have access to their init() functions.

// barba.js

import barba from '@barba/core';
import common from "./routes/common";
import somePage from "./routes/somePage";
// import ...

export default {
  init() {
    barba.init({
      transitions: [
        /* ... */
        {
          name: 'to-some-page',
          to: {
            namespace: ['some-page'],
          },
          leave: function (data) {
            /* ... */
          },
          enter: function (data) {
            // load JS
            common.containerInit();
            somePage.init();

            // barba behavior
            /* ... */
          },
        },
        /* ... */
      ],
    });
  },
};

We’ll also have to call this Barba initialization somewhere.
I choose to put it in main.js just after the router events have fired so it won’t do anything before we actually leave the page.

// main.js

// import external dependencies
import 'jquery';
    
// 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';
import somePage from './routes/somePage';
    
/** 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,
  // The new page we created
  somePage,
});
    
// Init barba && Load Events
jQuery(document).ready(() => {
  routes.loadEvents();
  myBarba.init();
});

There you have it!I choose to put it in main.js just after the router events have fired so it won’t do anything before we actually leave the page.
Our code is clean and organized, and you can use Barba secure in the knowledge that each piece of code will be executed at the right place and time.

Final Notes

Even if your code is working, there are a few frustrating issues you might still encounter.

Issue #1: Admin Bar

Unfortunately, Barba will attempt to load the links in the WordPress Admin Bar asynchronously as well.
This means clicking links in the Admin Bar won’t work, but it also means that trying to edit a post through the Admin Bar will just create drafts of that post.

According to the creator of Barba, the best solution to this problem is to disable Barba when logged in.

In order to do that, we’ll need to make three small changes to our theme.

First, let’s set up AJAX.
(More info about using AJAX in WordPress can be found here.)

// app/setup.php
// ...

add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('sage/main.js', asset_path('scripts/main.js'), ['jquery'], null, true);
    $ajax_params = array(
        'ajax_url' => admin_url('admin-ajax.php'),
        'ajax_nonce' => wp_create_nonce('my_nonce'),
    );

    wp_localize_script('sage/main.js', 'ajax_object', $ajax_params);
}, 100);

Second, add a function in functions.php.
This function will be called via AJAX and checks whether or not the user is logged in.

// functions.php
// ...

function ajax_check_user_logged_in() {
    echo is_user_logged_in();
    die();
}
add_action('wp_ajax_is_user_logged_in', 'ajax_check_user_logged_in');
add_action('wp_ajax_nopriv_is_user_logged_in', 'ajax_check_user_logged_in');

Third and finally, use the function we set up to conditionally load Barba for non-logged-in users.

// barba.js
// ...

$.post(ajax_object.ajax_url, {action: 'is_user_logged_in'}, function (isLogged) {
  if (!isLogged) barba.init(/* ... */);
});

Now the Admin Bar will work properly.

Issue #2: <body> Classes

Since Barba only modifies content inside of its container, classes on the <body> won’t change when using Barba to navigate between pages.
This can cause issues if CSS or JavaScript depends on those classes.

People on Roots Discourse found a smart workaround for this problem:
An HTML element inside your container where the classes will be loaded.

<!-- app.blade.php -->
<!-- ... -->

<div id="body-classes" @php(body_class())>

You can then append the classes to your <body> at the end of each page load.

// barba.js
// ...

barba.init({
  transitions: [
    {
      name: 'to-some-page',
      to: {
        namespace: ['some-page'],
      },
      leave: function (data) {
        /* ... */
      },
      enter: function (data) {
        gsap.from(data.current.container, 1, {
          opacity: 0, 
          onComplete: () => {
            this.async();

            $('body').attr('class', $('#body-classes').attr('class'));
          }
        });
      },
    },
  ],
});

Wrapping It Up

That should cover it! Apart from the above, I didn’t encounter any serious issues implementing this library.

Now that you’ve learned how to use Barba, let your imagination run wild and create all kinds of exciting transitions with pure CSS or with JavaScript animation libraries!

Barba has many other features that we didn’t touch on here, including caching, prefetching, and its own routing system.
You can find all of that in the official documentation.

If anything in this guide wasn’t clear or you have a way to further optimize this code, don’t hesitate to reach out to me @PJoy__ and I’ll be happy to incorporate your input.

Join the discussion on Roots Discourse

Help support our open-source development efforts

Help grow Roots

Join over 6,000 subscribers on our newsletter to get the latest Roots updates, along with occasional tips on building better WordPress sites.

Looking for WordPress plugin recommendations, the newest modern WordPress projects, and general web development tips and articles?

“Easily the best WordPress email I get.” Colin OBrien

Follow us on Twitter @rootswp

Ready to checkout?