Enhance your web page transitions

Add native-like page transitions to a website, progressive enhancement style, with barba.js and a little CSS.

Page transitions on the Web are typically very abrupt - you click a link, then the browser replaces the current page with the new one, as quickly as it can. This is a good starting point, and perhaps sometimes what we want, but a more gradual transition can be easier for people to follow. Visual transitions are what we are used to in the "real world"; sudden changes don't tend to happen, aside from perhaps a power outage (which is never welcome). Animating transitions can offer continuity and make it easier to retain context. In this article we are going to take some simple webpages, and progressively enhance the transitions, to take advantage of this.

Mobile apps are full of animated transitions. It feels to me that most fundamental amongst these are slide and push animations. So things either slide in and out of view over the top of the current content, or they slide in and push the current content off the screen. To cover these fundamentals, we will incorporate four transitions into our pages:

Here is what we are aiming for:

Our starting point is a collection of HTML pages (styled with CSS as you see fit). For the example in the video above this would be an index page and four subpages. The index page links to each subpage (the square icons). Each subpage links to the previous and next subpages, and back to the index page (the arrow icons). At this point we have a solid foundation to build upon, and not a million miles away from the finished interface.

To add animation to our page transitions we essentially need to get the content from the new page into the current page, without a page reload. A common approach to this is to build a single page application (SPA), but full blown SPAs bring with them at best added fragility, and at worst a diminished user experience.

An escalator can never break – it can only become stairs. You would never see an “Escalator Temporarily Out Of Order” sign, just “Escalator Temporarily Stairs. Sorry for the convenience. We apologize for the fact that you can still get up there.”

— Mitch Hedberg

For a progressive enhancement approach to this problem, we can use Barba.js - a small (7kb) JavaScript library for animating page transitions. To get started with Barba, we need some sort of JavaScript bundler, like Webpack or Rollup. rollup-starter-app is a good boilerplate if you don't already have a JavaScript bundling build step. We also need NPM or Yarn to install the required packages.

First we install Barba:

npm install @barba/core

By default Barba assumes we are going to use a JavaScript library for animation. Because we want to use CSS, we also need to install the CSS plugin:

npm install @barba/css

In our JS file, we import the packages, tell Barba to use the plugin, and initialise Barba:

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

// tell Barba to use the css plugin
barba.use(css);

// init Barba
barba.init();

We need to add a few things to our HTML so Barba understands the page structure. These things are:

All these are added with data attributes, like so:

<body data-barba="wrapper">
<!-- content that will not change (in our case the header) -->

<main data-barba="container" data-barba-namespace="company">
<!-- content that will be replaced when navigating -->
</main>

<!-- if we had a footer, it could go here -->
</body>

In the case of my example, I defined two namespaces, company for the index page, and product for the subpages. This enables us to specify transitions like so:

Another way to determine what transition is applied is by looking at the link which was clicked. We will use this method to determine whether the push transition should be left or right. To do that we need to make sure that the next and previous links have a class or attribute to identify them as such. In my case I used direction--prev and direction--next classes.

<a class="product-nav_item direction--prev" href=""></a>

Let's define those transitions now. We do that inside our init:

// init Barba
barba.init({
transitions: [{
name: 'slide-up-',
leave() {},
enter() {},
from: {
namespace: 'company'
},
to: {
namespace: 'product'
}
}, {
name: 'slide-down-',
leave() {},
enter() {},
from: {
namespace: 'product'
},
to: {
namespace: 'company'
}
}, {
name: 'slide-left-',
leave() {},
enter() {},
from: {
custom: ({ trigger }) => {
return trigger.classList && trigger.classList.contains('direction--prev');
}
},
to: {
namespace: 'product'
}
}, {
name: 'slide-right-',
leave() {},
enter() {},
from: {
custom: ({ trigger }) => {
return trigger.classList && trigger.classList.contains('direction--next');
}
},
to: {
namespace: 'product'
}
}]
});

There are four transitions configured here, each essentially containing a name, and conditions for when it should be used. The conditions are defined with the from and to rules. The simplest just using the namespaces, and a couple of them looking for those previous and next classes we set up earlier. The names given here are used by the Barba CSS plugin to create class names to use during transitions. For each transition defined, Barba will generate the following classes:

.[name]-leave {}
.[name]-leave-active {}
.[name]-leave-to {}
.[name]-enter {}
.[name]-enter-active {}
.[name]-enter-to {}

If you are wondering why I put hyphens at the end of my transition names, that's just a style preference. The resulting double hyphen in the class name, e.g slide-up--leave, differentiates it from a component class in the C3 CSS methodology.

Once you've added your bundled JavaScript to your pages, Barba should be up and running. You won't see much of a difference yet though, as the transitions have no styling. Let's add that in our CSS.

/* Slide up / down */

.slide-up--leave-active,
.slide-up--enter-active
{
transition: transform 250ms cubic-bezier(0.0, 0.0, 0.2, 1);
}

.slide-down--enter-active,
.slide-down--leave-active
{
transition: transform 200ms cubic-bezier(0.4, 0.0, 1, 1);
}

.slide-up--enter,
.slide-down--leave-to
{
transform: translateY(100%);
}

.slide-up--leave-to,
.slide-down--enter
{
transform: translateY(0);
}


/* Slide left / right */

.slide-left--leave-active,
.slide-left--enter-active,
.slide-right--leave-active,
.slide-right--enter-active
{
transition: transform 250ms cubic-bezier(0.4, 0.0, 0.2, 1);
}

.slide-left--leave,
.slide-left--enter-to,
.slide-right--leave,
.slide-right--enter-to
{
transform: translateX(0);
}

.slide-left--enter,
.slide-right--leave-to
{
transform: translateX(-100%);
}

.slide-left--leave-to,
.slide-right--enter
{
transform: translateX(100%);
}

Here we are using transforms to move our container, dependant upon what transition is being used and the current stage of that transition. Probably the best way to get your head around when these classes are applied is to set a long transition duration then inspect the page and watch the classes being applied as you click around. The transition timings (easing) I'm using here are taken from Material Design; you might want to look at other timings.

We're getting close now. With this CSS added, you should now have something like this:

It's starting to resemble what we are aiming for but with a couple of issues:

Both of these issues come down to the fact that the leave and enter parts of the animations are playing in sequence. What happens when we click a link is:

  1. Exit animation
  2. Content is replaced
  3. Enter animation

This would make sense for some transition effects, but for all of ours we need this:

  1. New content added to the page
  2. Exit and enter animations play together
  3. Old content removed

We can switch to this behaviour by enabling Barba's Sync mode. To do that we need to add sync: true to each of our transitions. For example:

barba.init({
transitions: [{
name: 'slide-up-',
sync: true,
leave() {},
enter() {},
from: {
namespace: 'company'
},
to: {
namespace: 'product'
}
}]
});

Now we have the sequence of events that we want, but we also have some new challenges. Barba is giving us the content container from two pages at the same time, but that's as far as it is going to help us. We now have to figure out how to style the second container. Things get a little less elegant here. We have a new block of content dumped in the DOM, breaking our layout. What needs to be done with this is probably going to change depending on your design, but you are almost certainly going to need to set this new container to position: absolute, which means it loses a lot of intrinsic sizing and positioning. We are going to need to do a little heavy lifting. Below is what I did. There are other ways to achieve the same thing; I'd be interested to hear how you would improve it.

.container + .container { /* second container */
position: absolute; /* remove it from flow */
top: 9rem; /* position after header (which is 9rem tall) */
min-height: calc(100vh - 9rem); /* at least height of viewport minus the header */
opacity: 0; /* prevent flash of content before transition starts */
}

.container + .container.slide-down--enter-to,
.container + .container.slide-left--enter-to,
.container + .container.slide-right--enter-to,
.container + .container.slide-up--enter-to
{
opacity: 1; /* while transition is active set opacity back to 1 */
}

.container + .container.slide-down--enter-to {
z-index: -1; /* this is later in the DOM but needs to sit behind */
left: 0; /* force full width grid */
right: 0;
}

That was a nasty little hurdle at the end, but we are pretty much at the finish line. One last tip: if you want to apply specific outside of the container, styling while transitions are in progress (to hide overflow for example), you can use hooks to add and remove classes:

barba.hooks.before(() => {
document.body.classList.add("transitioning--true");
});

barba.hooks.after(() => {
document.body.classList.remove("transitioning--true");
});

And that's it - quick native-like transitions, added to some humble HTML pages. You can see the end result here, (needs narrow viewport to see the links).

Webmentions

  1. Sam Smith Sam Smith
    What a nice surprise to find this post featured in the @css newsletter! 😀