Barba.js is a library using pushState
and AJAX allowing websites to load pages asynchronously while still maintaining browser states and history.
Combining it with some JavaScript animations libraries can help craft some memorable browsing experiences.
In this guide, we will try to find an optimal way to integrate Barba with our beloved Sage theme !
Note : This guide is using Sage v9.0.9 & Barba v2.9.7
- Barba
Hello, world !
- Animate the transition
- Namespaces
- Load page-specific JavaScript
- Organise the code
- Final notes
First, let's try to accomplish a simple Hello, world !
which won't display the famous line but instead initialize Barba behaviour for our website.
$ yarn add @barba/core
wrapper
and container
are concepts specific to Barba.
The content included in the wrapper
but not in the container
will not change throughout browsing.
The content included in the container
will be removed and loaded dynamically when links are clicked.
We will specify these blocs using data-*
attributes in our main template file.
<!-- app.blade.php -->
<html {!! get_language_attributes() !!}>
<!-- ... -->
<
@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 we have it initialised.
For now we will put our Barba code in common.js
so that it is initialised on every page. We will see later how we can optimise code structure.
// common.js
import barba from '@barba/core';
export default {
init() {
barba.init();
},
finalize() {
},
};
Go try it, your code should be working now ! Loading a new page shouldn't prompt the browser to reload while still loading the content and changing the active url :
It's not working !
- check that
data-*
attributes were effectively added to the HTML code - check that Barba is installed in
your_theme/node_modules/@barba/core
- check that Barba is initialised
- try to take a break and come back 😁
Now that Barba is up and running, let's try to implement a simple fade-out / fade-in effect for each container load (ie. new page load).
There are many libraries implementing animations, I personally really like anime.js by Julian Garnier but for the sake of consistency with Barba documentation we will use GSAP in this guide.
GSAP is a standard library for web animation but it's unfortunately not open-source.
Start by installing the library.
$ yarn add gsap
Then import
it in your code.
// common.js
import barba from '@barba/core';
import gsap from 'gsap';
export default {
init() {
barba.init({
/* ... */
});
},
finalize() {
},
};
First, let's add a basic
transition that we will chose to use for all pages (we will see later how to define different transitions for different pages).
We will specify the enter
and leave
functions, defining what will happen when the current container is removed (leave
) and when the next container is loaded (enter
).
We can access 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 GSAP functions .to()
and .from()
. Note that the second argument is the duration of the animation in seconds (GSAP .to()
doc here).
// 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 is instantly deleted !
Indeed ! leave
and enter
are immediately executed so we can't see the leave
animation.
Barba proposes many ways to implement synchronicity / asynchronicity, let's just pick one and not bother ! You are free to explore Barba docs to use the one you prefer.
Using this.async()
here makes it so that Barba waits for GSAP callback to call 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 close but it still doesn't work ! It seems like the first container stays on top of the other for a while before being deleted...
Yes, the first container is only removed after the whole transition is complete, it will stay on the page until then, unless we find a way to remove it !
You can access the parentNode
of the container
then 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 that this problem wouldn't have happened if we didn't use this.async()
in enter
function but this way is cleaner and leaves more room for future implementation of complex transitions.
Perfect !
If you plan to use Barba on your website, I suppose you're planning on doing fancier things than fading in/out for transitions and more importantly, you will want to have different transitions for different pages.
Luckily, Barba can help you with that !
namespace
is a simple Barba concept allowing you to define groups of pages for which Barba will have the same behaviour.
For now let's just define the namespace according to the current post_name
for the sake of the example. (In the future you might want to replace it by a specific function taking into account post types, categories, page names, 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>
In your JS code, you will now be able to write specific Barba transitions for your own cases :
- when you're going to a specific page :
to
- when you're coming from a specific page :
from
- a combination of both cases :
to
+from
Note : this has nothing to with GSAP
.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
here 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.
Here is where things begin to get a little bit more complicated.
From this point we were able to :
- setup Barba on our site
- setup animations
- setup multiple transitions for different namespaces
But we saw that Barba only loads content contained inside the containers, so what about JS code for each page which is located after the global <footer>
and therefore not loaded nor executed after each transition ?
We will see what we can do about that but first, let's take a look at Sage JS routing system
Sage implements a simple routing system for JavaScript files which does basically 2 things :
- Select what route will be called according to WordPress
<body>
classes - Execute route events in a specific order, which is :
common init
page-specific init
page-specific finalize
common finalize
This system also has the benefit of allowing us to split our JS code between different files, each one corresponding to our different pages.
For more info about this I recommend you just open main.js
and router.js
since it is pretty much straightforward and the code is well explained.
You might need this at some point so I will just explain it here :
- create a new file for your route :
routes/somePage.js
- fill the content :
// 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,
});
Keep in mind that your <body>
will need to have the class somePage
(or some-page
) if you want the code for this route to be loaded and executed. Also you will probably encounter some issues with <body>
classes not updating once Barba is all set up, I explain how to fix this here.
Anyway, back to our problem, how are we going to connect this system to Barba ?
Basically our problem now with Barba is that our JS code doesn't load when we change pages. What we want to have is the following :
When we enter a page without Barba (via direct access or if Barba is disabled), load :
- common JS code for the whole page (container included)
- page-specific JS code
When we enter a page with Barba, load :
- common JS code for the container only
- page-specific JS code
We are assuming here that page-specific JS only affects content inside the container
The first thing we'll do is split our common.js
code in two parts :
- one for the "global" JS outside the containers (header, menus, footer, ...) :
init()
- one for JS inside the containers that must be loaded for each page :
containerInit()
containerInit()
will be called in the initial init()
function but also on Barba enter
trigger. Rest assured, 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 will work ! But... This code doesn't feel very clean does it ?
We have to include all our pages in common.js, removing the semantic separation between routes. Also it seems like common.js
will be full of Barba settings, moving all this code to an external file would certainly be better...
So let's do it !
First we want to have a single file with all our Barba code, we will import
all our routes there so that we can call their init()
functions when needed.
// 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
/* ... */
},
},
/* ... */
],
});
},
};
Then we have to add our Barba code somewhere in the execution pipeline so it's taken into account. I chose to do it in main.js
after the other events since 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();
});
And there we have it ! We can now use Barba in a clean fashion, knowing that every bit of code needed will be executed according to the user actions.
Even if your code is now functional, there are still some problems that might upset you.
One issue you'll encounter using Barba with WordPress is that the asynchronous loading also happens when clicking on links from the admin bar. Firstly the admin pages will fail to load and secondly, trying to edit posts this way will end up in creating draft copies of your post in WordPress back-office...
According to Barba creator the best thing to do to solve this issue is simply to disable Barba when logged in.
To do so, we'll have to add these 3 bits of code into our project.
First let's enable us to use AJAX by giving JS acces to the AJAX URL (more info about using AJAX in WordPress 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);
Then add a function in functions.php
, which will be called via AJAX, to check if 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');
Finally access this function in JS and disable Barba if needed.
// barba.js
// ...
$.post(ajax_object.ajax_url, {action: 'is_user_logged_in'}, function (isLogged) {
if (!isLogged) barba.init(/* ... */);
});
Your users will now be able to use the admin bar properly.
Since Barba only loads content in the container
, <body>
classes won't change when entering a specific page. This can cause some issues if we're actually using these classes in CSS (and jQuery) selectors.
People on roots discourse actually found a smart workaround for this issue. The idea is to simply have an HTML element inside your container
where the classes will be loaded.
<!-- app.blade.php -->
<!-- ... -->
<div id="body-classes" @php(body_class())></div>
You can then append the classes to your <body>
element 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'));
}
});
},
},
],
});
I think this pretty much does it, I didn't encounter other real issues while using the lib.
Now you can just let your imagination run wild and create all kind of transitions with pure CSS or animation libraries !
Barba has much more functionalities such as caching, prefetching , and routing. You will find all of these in the official documentation.
If some parts aren't clear enough or if you find some more optimizations, don't hesitate to tell me and I will include your remarks in the guide.
Half way through this tutorial you start referring to the next container as the current container. The result being that when testing, the fade out animation works but the fade in animation on the next page does not. All enter functions should refer to the 'next' container not the 'current'.