Menu Search
Jump to the content X X
Smashing Conf New York

You know, we use ad-blockers as well. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. our upcoming SmashingConf New York, dedicated to smart front-end techniques and design patterns.

Improving User Flow Through Page Transitions

Any time a user’s experience is interrupted, the chance of them leaving increases. Changing from one page to another will often cause this interruption by showing a white flash of no content, by taking too long to load or by otherwise taking the user out of the context they were in before the new page opened.

Transitions between pages can enhance the experience by retaining (or even improving) the user’s context, maintaining their attention, and providing visual continuity and positive feedback. At the same time, page transitions can also be aesthetically pleasing and fun and can reinforce branding when done well.

Further Reading on SmashingMag: Link

In this article, we’ll create, step by step, a transition between pages. We will also talk about the pros and cons of this technique and how to push it to its limit.

Examples Link

Many mobile apps make good use of transitions between views. In the example below, which follows Google’s material design4 guidelines, we see how the animation conveys hierarchical and spatial relationships between pages.


Example of transition from Google’s material design

Why don’t we use the same approach with our websites? Why are we OK with the user feeling like they are being teleported every time the page changes?

How To Transition Between Web Pages Link

SPA Frameworks Link

Before getting our hands dirty, I should say something about single-page application (SPA) frameworks. If you are using an SPA framework (such as AngularJS, Backbone.js or Ember), then creating transitions between pages will be much easier because all of the routing is already handled by JavaScript. Please refer to the relevant documentation to see how to transition pages using your framework of choice, because there are probably some good examples and tutorials.

The Wrong Way Link

My first attempt to create a transition between pages looked more or less like this:

document.addEventListener('DOMContentLoaded', function() {
  // Animate in
});

document.addEventListener('beforeunload', function() {
  // Animate out
});

The concept is simple: Use one animation when the user leaves the page, and another animation when the new page loads.

However, I soon found that this solution had some limitations:

  • We don’t know how long the next page will take to load, so the animation might not look fluid.
  • We can’t create transitions that combine content from the previous and next pages.

In fact, the only way to achieve a fluid and smooth transition is to have full control over the page-changing process and, therefore, not to change the page at all. Thus, we have to change our approach to the problem.

The Right Way Link

Let’s look at the steps involved in creating a simple crossfade transition between pages the right way. It involves something called pushState AJAX (or PJAX) navigation, which will essentially turn our website into a kind of single-page website.

Not only does this technique achieve smooth and pleasant transitions, but we will benefit from other advantages, which we will cover in detail later in this article.

The first step is to create a click event listener for all links to use, preventing the browser from performing its default behavior and customizing the way it handles page changes.

// Note, we are purposely binding our listener on the document object
// so that we can intercept any anchors added in future.
document.addEventListener('click', function(e) {
  var el = e.target;

  // Go up in the nodelist until we find a node with .href (HTMLAnchorElement)
  while (el && !el.href) {
    el = el.parentNode;
  }

  if (el) {
    e.preventDefault();
    return;
  }
});

This method of adding an event listener to a parent element, instead of adding it to each specific node, is called event delegation5, and it’s possible due to the event-bubbling nature6 of the HTML DOM API.

Fetch the Page Link

Now that we have interrupted the browser when it tries to change the page, we can manually fetch that page using the Fetch API7. Let’s look at the following function, which fetches the HTML content of a page when given its URL.

function loadPage(url) {
  return fetch(url, {
    method: 'GET'
  }).then(function(response) {
    return response.text();
  });
}

For browsers that don’t support the Fetch API, consider adding the polyfill8 or using the good old-fashioned XMLHttpRequest9.

Change the Current URL Link

HTML5 has a fantastic API called pushState10, which allows websites to access and modify the browser’s history without loading any pages. Below, we are using it to modify the current URL to be the URL of the next page. Note that this is a modification of our previously declared anchor click-event handler.


if (el) {
  e.preventDefault();
  history.pushState(null, null, el.href);
  changePage();

  return;
}

As you might have noticed, we have also added a call to a function named changePage, which we will look at in detail shortly. The same function will also be called in the popstate event, which is fired when the browser’s active history entry changes (as when a user clicks on the back button of their browser):


window.addEventListener('popstate', changePage);

With all of this, we are basically building a very primitive routing system, in which we have active and passive modes.

Our active mode is in use when a user clicks on a link and we change the URL using pushState, while passive mode is in use when the URL changes and we get notified by the popstate event. In either case, we are going to call changePage, which takes care of reading the new URL and loading the relevant page.

Parse and Add the New Content Link

Typically, the pages being navigated will have common elements, like header and footer. Suppose we use the following DOM structure on all of our pages (which is actually the structure of Smashing Magazine itself):


<header>
  …
</header>

<main>
  <div class="cc">
     …
  </div>
</main>

<footer>
 …
</footer>

The only part we need to swap at every page change is the content of the cc container. Thus, we can structure our changePage function like so:


var main = document.querySelector('main');

function changePage() {
  // Note, the URL has already been changed
  var url = window.location.href;

  loadPage(url).then(function(responseText) {
    var wrapper = document.createElement('div');
        wrapper.innerHTML = responseText;

    var oldContent = document.querySelector('.cc');
    var newContent = wrapper.querySelector('.cc');

    main.appendChild(newContent);
    animate(oldContent, newContent);
  });
}

Animate! Link

When the user clicks a link, the changePage function fetches the HTML of that page, then extracts the cc container and adds it to the main element. At this point, we have two cc containers on our page, the first belonging to the previous page and the second from the next page.

The next function, animate, takes care of crossfading the two containers by overlapping them, fading out the old one, fading in the new one and removing the old container. In this example, I’m using the Web Animations API11 to create the fade animation, but of course you can use any technique or library you’d like.


function animate(oldContent, newContent) {
  oldContent.style.position = 'absolute';

  var fadeOut = oldContent.animate({
    opacity: [1, 0]
  }, 1000);

  var fadeIn = newContent.animate({
    opacity: [0, 1]
  }, 1000);

  fadeIn.onfinish = function() {
    oldContent.parentNode.removeChild(oldContent);
  };
}

The final code is available on GitHub12.


Result of crossfade transition between pages on Smashing Magazine

And those are the basics of transitioning web pages!

Caveats And Limitations Link

The little example we’ve just created is far from perfect. In fact, we still haven’t taken into account a few things:

  • Make sure we affect the correct links.
    Before changing the behavior of a link, we should add a check to make sure it should be changed. For example, we should ignore all links with target="_blank" (which opens the page in a new tab), all links to external domains, and some other special cases, like Control/Command + click (which also opens the page in a new tab).
  • Update elements outside of the main content container.
    Currently, when the page changes, all elements outside of the cc container remain the same. However, some of these elements would need to be changed (which could now only be done manually), including the title of the document, the menu element with the active class, and potentially many others depending on the website.
  • Manage the lifecycle of JavaScript.
    Our page now behaves like an SPA, in which the browser does not change pages itself. So, we need to manually take care of the JavaScript lifecycle — for example, binding and unbinding certain events, reevaluating plugins, and including polyfills and third-party code.

Browser Support Link

The only requirement for this mode of navigation we’re implementing is the pushState API, which is available in all modern browsers13. This technique works fully as a progressive enhancement. The pages are still served and accessible in the usual way, and the website will continue to work normally when JavaScript is disabled.

If you are using an SPA framework, consider using PJAX navigation instead, just to keep navigation fast. In doing so, you gain legacy support and create a more SEO-friendly website.

Going Even Further Link

We can continue to push the limit of this technique by optimizing certain aspects of it. The next few tricks will speed up navigation, significantly enhancing the user’s experience.

Using a Cache Link

By slightly changing our loadPage function, we can add a simple cache, which makes sure that pages that have already been visited aren’t reloaded.


var cache = {};
function loadPage(url) {
  if (cache[url]) {
    return new Promise(function(resolve) {
      resolve(cache[url]);
    });
  }

  return fetch(url, {
    method: 'GET'
  }).then(function(response) {
    cache[url] = response.text();
    return cache[url];
  });
}

As you may have guessed, we can use a more permanent cache with the Cache API14 or another client-side persistent-storage cache (like IndexedDB).

Animating Out the Current Page Link

Our crossfade effect requires that the next page be loaded and ready before the transition completes. With another effect, we might want to start animating out the old page as soon as the user clicks the link, which would give the user immediate feedback, a great aid to perceived performance.

By using promises15, handling this kind of situation becomes very easy. The .all method creates a new promise that gets resolved as soon as all promises included as arguments are resolved.


// As soon as animateOut() and loadPage() are resolved…
Promise.all[animateOut(), loadPage(url)]
  .then(function(values) {
    …

Prefetching the Next Page Link

Using just PJAX navigation, page changes are usually almost twice as fast as default navigation, because the browser does not have to parse and evaluate any scripts or styles on the new page.

However, we can go even further by starting to preload the next page when the user hovers over or starts touching the link.

As you can see, there are usually 200 to 300 milliseconds of delay in the user’s hovering and clicking. This is dead time and is usually enough to load the next page.

That being said, prefetch wisely because it can easily become a bottleneck. For example, if you have a long list of links and the user is scrolling through it, this technique will prefetch all of the pages because the links are passing under the mouse.

Another factor we could detect and take into account in deciding whether to prefetch is the user’s connection speed. (Maybe this will be made possible in future with the Network Information API17.)

Partial Output Link

In our loadPage function, we are fetching the entire HTML document, but we actually just need the cc container. If we are using a server-side language, we can detect whether the request is coming from a particular custom AJAX call and, if so, output just the container it needs. By using the Headers API18, we can send a custom HTTP header in our fetch request.


function loadPage(url) {
  var myHeaders = new Headers();
  myHeaders.append('x-pjax', 'yes');

  return fetch(url, {
    method: 'GET',
    headers: myHeaders,
  }).then(function(response) {
    return response.text();
  });
}

Then, on the server side (using PHP in this case), we can detect whether our custom header exists before outputting only the required container:


if (isset($_SERVER['HTTP_X_PJAX'])) {
  // Output just the container
}

This will reduce the size of the HTTP message and also reduce the server-side load.

Wrapping Up Link

After implementing this technique in a couple of projects, I realized that a reusable library would be immensely helpful. It would save me time in implementing it on each occasion, freeing me to focus on the transition effects themselves.

Thus was born Barba.js2119, a tiny library (4 KB minified and gZip’d) that abstracts away all of this complexity and provides a nice, clean and simple API for developers to use. It also accounts for views and comes with reusable transitions, caching, prefetching and events. It is open source and available on GitHub20.


Demo of a transition created with Barba.js2119

Conclusion Link

We’ve seen now how to create a crossfade effect and the pros and cons of using PJAX navigation to effectively transform our website into an SPA. Apart from the benefit of the transition itself, we’ve also seen how to implement simple caching and prefetching mechanisms to speed up the loading of new pages.

This entire article is based on my personal experience and what I’ve learned from implementing page transitions in projects that I’ve worked on. If you have any questions, do not hesitate to leave a comment or reach out to me on Twitter — my info is below!

(rb, ml, al, il)

Footnotes Link

  1. 1 https://www.smashingmagazine.com/2013/10/smart-transitions-in-user-experience-design/
  2. 2 https://www.smashingmagazine.com/2013/02/designing-in-transition-to-multi-device/
  3. 3 https://www.smashingmagazine.com/2014/10/providing-a-native-experience-with-web-technologies/
  4. 4 https://material.google.com/motion/material-motion.html
  5. 5 http://stackoverflow.com/a/1688293/2065702
  6. 6 http://stackoverflow.com/a/4616720/2065702
  7. 7 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
  8. 8 https://github.com/github/fetch
  9. 9 https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
  10. 10 https://developer.mozilla.org/en-US/docs/Web/API/History_API
  11. 11 https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
  12. 12 https://gist.github.com/luruke/0704bc594c81e3f4c491ba919b96450a
  13. 13 http://caniuse.com/#feat=history
  14. 14 https://developer.mozilla.org/en-US/docs/Web/API/Cache
  15. 15 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
  16. 16 http://instantclick.io/click-test
  17. 17 http://wicg.github.io/netinfo/
  18. 18 https://developer.mozilla.org/en-US/docs/Web/API/Headers
  19. 19 http://barbajs.org
  20. 20 https://github.com/luruke/barba.js
  21. 21 http://barbajs.org

↑ Back to top Tweet itShare on Facebook

Luigi De Rosa works as Senior Front-End Developer at EPIC in Liège, Belgium. He is passionate about creative development, animations, JavaScript and performance optimization. You can follow him on GitHub or Twitter.

  1. 1

    Adrian Florescu

    July 5, 2016 10:23 am

    Nice article, this would’ve helped me a lot when I was trying to create something similar: Here is a demo: http://codepen.io/anything/pen/grBKwr

    I’ve also written an article about it: https://medium.com/vivre-tech/boo-peek-a-boo-17af2ce75357

    6
  2. 2

    I can’t believe what utterly terrible advice this article is offering. Please, nobody ever do these things.

    -28
    • 3

      Luigi De Rosa

      July 5, 2016 10:42 am

      Hi, thanks for your message!
      Can you motivate your comment?
      Why do you think these are terrible advices?

      Cheers!

      15
      • 4

        Animations can be bad for accessibility. It would be smart to add into your project, if it doesn’t already exist, the ability for the user to “turn off” the animations while still leveraging the rest of the gains.

        While I do like the idea in general, typically the overhead can be troubling with Javascript heavy pages. It can require a bit of rethinking the way you write your JS when it comes to naming functions, variables, ids, classes, event delegation etc.

        I would argue the large negative of SPAs is the golden rule that people spend more time on other people’s sites. So when yours is different, or behaves different, it can be difficult and/or cumbersome for the user. Personally I would aim for it to be as visually the same as other sites as possible and use it as a performance gain at best.

        -11
    • 5

      Struggling to see why this is terrible advice – It is fully progressively enhanced, so will still work if the required feature(s) aren’t available, and it has the benefit of actually making your site (appear to) load faster. I would understand your comment if this was a breaking enhancement, but it isn’t.

      Perhaps you could elaborate on why this is such a bad thing?

      17
      • 6

        Stan Rogers

        July 5, 2016 7:35 pm

        It will also work if the “desired features” are undesired. I obviously can’t speak for all users, but I don’t want things to “(appear to) load faster”, or to be kept entertained while the saggy page does its thing. Howzabout actually making things faster instead? Page turning (and the momentary lack of content it engenders) isn’t exactly the traumatic experiece it’s made out to be; generations of people have survived it and gone on to lead happy, well-adjusted lives. Downloading less stuff that renders much more quickly while using far fewer resources is, or at least *should be*, the goal here.

        1
        • 7

          Alright, but this technique can actually limit the amount of redundant processing done on the client side, therefore “actually making things faster”. When you load up a page, every script and stylesheet has to be executed completely (even if it is cached, it still has to be executed). If you are using any kind of framework, that can become expensive.

          I had to build a medium-sized web application that required a lot of bootstrap-like components, and on top of that painted interactive responsive graphs. Even when I cached the pages server side, they felt slow because of the initialization required on every page load.

          When I added a SPA layer on top the pages loaded much faster because everything was already in memory and ready. I didn’t have to “trick” the users into thinking it was faster with transitions, it *was* faster because it let the browser do less work.

          Sure I could have theoretically written my own libraries that only used what I needed and initialized fast, but that was an unrealistic amount of work for one developer under a deadline. I went for the low-hanging fruit that had the best return on investment and the users benefited.

          20
  3. 8

    Nice article. I’ve been working on something similar, but using a worker to fetch links in a separate thread rather than doing all the heavy lifting on the main thread so as to allow everything else to continue without needing to worry about what the fetch script is doing. Yours will use less data than mine, because my prefetch will fetch the html for any (internal) links it finds in a page, rather than just ones that are being hovered… It’s a bit of a “damned if you do, damned if you don’t” situation, really – My version is potentially more costly on mobile data, and it probably wouldn’t be appropriate for large websites with a lot of links. From the fact that it prefetches on hover, though, your solution won’t do any prefetching on mobile, thus negating the perceived speed increase.

    I’ve chosen to keep the actual transitions part of the equation (almost) entirely separate from the prefetching, allowing the user to deal with the transitions using whatever animation library etc they see fit. I like to keep things separate, so that each script only does its own thing – It makes the solution more pluggable in the end.

    It’s funny that this should be published now, just as I have been working on the same problem, though. Looking around the web, I don’t really see any viable solutions outside of going full SPA, which I didn’t want to do for several reasons. There is a jQuery plugin called SmoothState, but I didn’t want to use jQuery, so had to solve the problem a different way.

    I think that this kind of javascript-enhanced navigation will only increase in the future, as designers and developers look for ways to make their experiences more fluid and engaging. I’m sure this article will be a great resource for a lot of developers in the next few months. Nice one.

    6
    • 9

      Luigi De Rosa

      July 5, 2016 1:17 pm

      Hi there!
      Thanks for your comment.

      Nice idea to use a worker to fetch the page.

      As wrote in the article, I made Barba.js, maybe can suit your needs :)
      https://github.com/luruke/barba.js

      It’s jQuery free and the transitions are isolated object, you can use any technique/library to make the transitions.

      3
  4. 10

    Extracted from the library page: “This technique consist in preventing the normal link behavior, changing manually the browser url, and injecting manually the new content in the page.”

    Sounds like too much work to replace something that already works perfectly fine.

    It’s a cool experiment, I can see it working in some very specific contexts, but I hope it’s not aiming to be an “universal solution”.

    Thanks for the article, Luigi.

    4
    • 11

      Luigi De Rosa

      July 6, 2016 1:53 pm

      Hello jpmelguizo,

      Yes, that’s a lot of work if you have to implement this technique every time in your website.

      This is one of the reason I made [Barba.js](https://github.com/luruke/barba.js), to be able to focus on the transition itself without spending too much time on the technique itself.

      3
  5. 12

    Great article, I was looking for something like this! And it doesn’t ruin your SEO like SPA’s

    4
  6. 15

    The idea is definitely great from a user experience standpoint – people first, right? Less friction, a more natural approach that doesn’t break the fluidity of the user experience. But is there any sort of penalties from Google for “breaking” the browser’s default behavior? If anybody knows, I’d love to know.

    3
    • 16

      Luigi De Rosa

      July 5, 2016 5:39 pm

      Hi Julien,
      I’m not a SEO expert, but I can freely say that with this technique for Google (or other crawler/bots) nothing changes.

      Each page is served normally, and the website works normally without JavaScript.

      4
  7. 18

    Darko Arnautovic

    July 6, 2016 7:32 am

    Is it just me or did anyone else’s eyes feel tired while watching the transitions on smashing mag demo? Think it would be a bit better if they were a little shorter.

    Thanks for the article. Will definitely check out the library.

    1
    • 19

      Luigi De Rosa

      July 6, 2016 1:51 pm

      Hello Darko, Definitely.
      One second is transition is probably too much.

      But the transition on Smashing Magazine was just a demo :)

      Thank you for your comment.
      Cheers!

      2
  8. 20

    Michał Sadowski

    July 6, 2016 1:01 pm

    This is, indeed, a very nice technique that can serve as a great way to introduce spatial relationship of objects (I made a site where tiles “grow” into hero images and then rest of the content rolls in).
    The biggest problem I’ve found is that when you want to “roll back” (user initiates popstate to return to the previous site), browsers use almost completely unpredictable way of jumping the scroll. I haven’t found a decent way to control the way page scrolls.

    0
    • 21

      Luigi De Rosa

      July 6, 2016 1:49 pm

      Hi Michał!
      Thanks for your comment.

      Honestly I never had those kind of problem, do you have a little reproducible demo?

      Cheers

      0
  9. 22

    Why didn’t you recommend Google’s SPFJS instead? https://github.com/youtube/spfjs

    -1
    • 23

      Luigi De Rosa

      July 6, 2016 4:22 pm

      Thanks!
      I was not aware of spfjs.
      I will definitely check it out.

      Thanks for mentioning it.
      Cheers

      1
  10. 24

    I’ve been thinking about something like as an enhancement for my site. Thanks for the sharing!

    3
  11. 25

    Eric Njanga

    July 23, 2016 4:52 pm

    Great article buddy. I was mostly interested on hearing what you had to say about the pros an cons of PJAX navigation. Thanks.

    3
  12. 26

    Where can I find coding for the above video please?

    0
  13. 27

    Hi @Luigi, thank you for your article, can i ask you few questions?
    Is your library Barba.js can i use wp_enqueue_scripts and add your library
    in my WordPress site, or do i need extra steps for making this work with cms?
    I was searching for something similar on github, and i found newest pjax-api package, but i think its depend on jQuery
    Do anyone know more pjax packages working without jQuery?
    Sorry for my English ;/

    2
  14. 28

    Daan Weustenraad

    September 28, 2016 11:42 am

    Hi Luigi,

    I used your script on some simple html to see how the transition works, but it seems to flicker in Firefox when I use it.

    See my demo here: http://daan.onl/dev/js/page-transition/index.html

    Any clues on how to improve this?

    Thanks!

    1

↑ Back to top