~ / Dan Schlosser / projects /

Published on August 11, 2017.

I was working on behalf of Minimill, building a site for Albert Wenger's latest book, World After Capital. It was a simple static site: a handful of HTML pages with some shared CSS. I had finished the basic implementation, but I felt something was missing. It needed motion. The design used a small set of components across each of the pages. A blue (and sometimes white) background, a striking yellow book cover, and a consistent navigation bar in the upper right.

The home, author, talks, and FAQ pages of worldaftercapital.org.

I wanted to create animations between each page, each component morphing into different forms as the user navigates through the site. However, this presented a problem. Typically, in order to animate across page transitions on the web, you need a single page app, where all of the content and structure of the site is loaded at once and each page transition just hides (and shows) different parts of the page. My problem was, I already had a multi-page static website, and I didn't want to rewrite the entire thing to add these page transitions. As a result, I was going to have to account for a browser refresh between each page.

To solve this problem, I wrote a JavaScript library that choreographs entrance and exit animations between multiple website pages, hides the browser refresh, and removes the need for a single page app.

How it works

In order for pangea.js to work, the web designer must first take every pair of pages, imagine a common visual state between them, and code each one in CSS. After instatiating the library on each page, pangea.js handles the rest. When you click a link, the following happens:

  1. The library inspects the link's href property, to figure out where you're trying to go.
  2. If the destination is a known page with a shared state, add a class to the body, triggering CSS that animates the page to the shared visual state.
  3. Once in the shared state, the page refresh is triggered by modifying window.location.
  4. On every new page, trigger entrance animations from the shared visual state to the new page.

The effect is pretty smooth:

The result is one fluid animation between pages and a hidden page refresh. (You can find the refresh by watching Chrome's URL label in the lower left corner.) By animating to and from a simple shared state and leveraging the browser's asset caching, the browser's page change often takes less than 100ms, the lower limit for human-perceptable delay.

Using the library

The first step for using pangea.js is writing markup. In order for the library to know when a page animation is complete, simply mark an element on the page with an ID. Here, I use last-to-animate.

<!-- 1. Include the pangea.js library -->
<script src="pangea.min.js"></script>

<!-- 2. Tag an element as being the last to animate, using any ID you see fit -->
<h1>Test webpage</h1>
<p><a href="/about">More about me</a></p>
<p id="last-to-animate">This element is last to animate</p>

Next, for each page, add styles that react to various classes being added to the <body>. In order to achieve smooth animations, this will require one entrance animation per page from the shared state, and one exit animation to the shared state for each page the source page links to.

 * Example default styles.
body {
    backgrond-color: white;
    /* We use CSS transitions to create the animations. */
    transition: 0.3s ease background;

h1, p {
    opacity: 1;
    transition: 0.3s ease opacity;

 * Example animation styles.
 * Here, we fade out text and fade the background to black when the
 * Pangea library gives the body the animating-to-about-page class.
body.animating-to-about-page {
    background-color: black;

body.animating-to-about-page h1,
body.animating-to-about-page p {
    opacity: 0;

Finally, instantiate the pangea.js library. On each page, register each destination page that should be tracked, the body class to trigger the animation, and the last element to animate.

 * When we click links to the /about page, the library gives the body the
 * animating-to-about-page class.  When the element with ID last-to-animate
 * is done transitioning, we will navigate to the /about page.
var pangea = new Pangea()
    .register(/\/about/, 'last-to-animate', 'animating-to-about-page')

As you may notice, the syntax is somewhat verbose; this is an intentional trade-off. Because there is no shared communication between pages on this type of flat HTML site, each pair of pages must be manually registered and styled. This results in a tiny package size of just 1.4KB minified and gzipped.

The pangea.js logo, courtesy of Jeff Hilnbrand.


One of the tricky things about using pangea.js is achieving the sub-100ms subsequent page load times. It was easy on worldaftercapital.org because the site was relatively lightweight. I was worried that it wouldn't scale to heavier static sites. So, when we decided to redesign Minimill's portfolio site, we made transitions a centerpiece:

Minimill's site is much heavier than World After Capital's. However, the browser does a lot of heavy lifting, creating a seamless animation between each page. In the case of the project sub-pages, they even share an image (the open laptop) which sees no flicker.

We could have built Minimill's site as a single page application, but then we pay the cost of loading a large JavaScript bundle at the initial load. This means major slowdown, especially on mobile or with poor connectivity. With pangea.js, we essentially disable the entire library on mobile, and the site behaves as you'd expect. No animations, just regular links between HTML.

There are certainly cases where pangea.js isn't perfect, but for flat, static websites that place a priority on performance and mobile experience, I think it's an elegant solution.

Like all of Minimill's tooling, pangea.js is open source. Please check out the documented source, and submit an issue or pull request if you think it could work even better. Thanks in advance!

View pangea.js on GitHub

/ Projects