Animating Turbolinks

in CSS , JavaScript

I’ve been using Turbolinks (with jquery.turbolinks) in a lot of my projects recently in an attempt to keep things fast while still doing most all of the rendering on the server. I like that Turbolinks more or less works out of the box without a ton of dependencies or set up and, IMO, speeds up page renders. The problem I ran up against was to add page transitions (like a swipe effect) to mobile pages. Since Turbolinks just swaps the body of the document without any incremental steps, it’s a little tricky to effectively animate that transition. The way I ended up overcoming it was to use Turbolinks’ event emissions to add classes to the HTML node and animate a child node with CSS transforms.

The CoffeeScript:

 1# starting to fetch a new target page
 2$(document).on 'page:fetch', ->
 3  $(document.documentElement)
 4    .removeClass('loaded')
 5
 6  $(document.documentElement)
 7    .addClass('loading')
 8
 9# the page has been parsed and changed to the new version and on DOMContentLoaded
10$(document).on 'page:change', ->
11  $(document.documentElement)
12    .removeClass('loading')
13
14
15# is fired at the end of the loading process
16$(document).on 'page:load', ->
17  $(document.documentElement)
18    .addClass('loaded')

The SCSS (the @media rule keeps this on mobile-only):

 1@media (max-width: $grid-float-breakpoint) {
 2  $duration: 0.15s;
 3  $swing: 150%;
 4
 5  #page-content {
 6    @include transform(translate3d($swing, 0, 0));
 7
 8    .loaded & {
 9      @include transform(none);
10      @include transition(transform $duration ease-in-out);
11    }
12
13    .loading & {
14      @include transform(translate3d(-#{ $swing }, 0, 0));
15      @include transition(transform $duration ease-in-out);
16
17      &:before {
18        @include transform(translate3d($swing, 0, 0));
19
20        -webkit-animation: pulse 3s infinite ease-in-out;
21        -moz-animation: pulse 3s infinite ease-in-out;
22        content: 'Loading...';
23        display: block;
24        font-size: 28px;
25        left: 0;
26        position: fixed;
27        text-align: center;
28        top: 100px;
29        width: 100%;
30      }
31    }
32  }
33}
34
35@-webkit-keyframes pulse {
36  0%   { opacity: 0; }
37  50%  { opacity: 0.3; }
38  100% { opacity: 0; }
39}
40@-moz-keyframes pulse {
41  0%   { opacity: 0; }
42  50%  { opacity: 0.3; }
43  100% { opacity: 0; }
44}

So, we:

  1. On page:fetch add the ’loading’ class to the HTML, causing the #page-content node to swing hard left (the negative X translation) and appear to ‘swipe away’ off-screen (that’s the transition).
  2. On page:change we remove ’loading’ from the HTML, causing #page-content to swing hard right (the positive X translation) so it resides in the wings of the page, invisible until…
  3. We add the ’loaded’ class to the HTML, removing any translation on the #page-content node, animating it into center position where it looks just fine and normal.

A note on Bootstrap: I’d used translate3d(0, 0, 0) for the ’loaded’ state before but it totally breaks any instances of the Bootstrap modal whereas the transform(none) doesn’t. The modals use a translate3d effect to slide them into view and for whatever reason the stacking of these effects doesn’t play nice in Chrome.