Lovely, Smooth Page Transitions With the History Web API

In this tutorial we’re going to build a website with beautifully smooth transitioning pages, without the usual aggressive page refresh. Navigate through the pages in the demo to see what I mean.
To achieve this effect we’ll use the History Web API. In a nutshell, this API is used to alter the browser history. It allows us to load a new URL, change the page title, then at the same time record it as a new visit in the browser without having to actually load the page.
This sounds confusing, but it opens up a number of possibilities–such as being able to serve smoother page transitions and give a sense of speediness which improves the user experience. You have probably already witnessed the Web History API in action on a number of websites and web applications, such as Trello, Quartz, and Privacy.



Before we go any further, let’s first look into one particular API that we are going deploy on the website.
The History Web API, in Brief
To access the Web History API, we first write window.history
then follow this with one of the APIs; a method or a property. In this tutorial we’ll be focusing on the pushState()
method, so:
1 |
window.history.pushState( state, title, url ); |
As you can see from the above snippet, the pushState()
method takes three parameters.
- The first parameter,
state
, should be an object containing arbitrary data. This data will then be accessible throughwindow.history.state
. In a real world application, we would pass data like a page ID, a URL, or serialized inputs derived from a form. - The last two parameters are
title
and -
url
. These two change the URL and the document title in the browser, as well as record them as a new entry in the browser history.
Let’s dissect the following example to better understand how the pushState()
Method works.
1 |
(function( $ ){ |
2 |
|
3 |
$( "a" ).on( "click", function( event ) { |
4 |
|
5 |
event.preventDefault(); |
6 |
|
7 |
window.history.pushState( { ID: 9 }, "About - Acme", "about/" ); |
8 |
} ); |
9 |
|
10 |
})( jQuery ); |
In the above code, a link attached with the click
event then deploys the pushState()
method. As we click on the link, we expect the code to change the document title and the URL:



And it does; the screenshot shows the URL is changed to “about/” as defined in the pushState()
method. And since the pushState()
method creates a new record in the browser history, we are able to go back to the previous page through the browser’s Back button.
However, all the browsers in this example are currently ignoring the title
parameter. You can see from the screenshot the document does not change to About - Acme as specified. Furthermore, calling the pushState()
method won’t also trigger the popstate
event; an event which is dispatched every time the history changes–something we need! There are a few discrepancies on how browsers handle this event, as stated in MDN:
“Browsers tend to handle thepopstate
event differently on page load. Chrome (prior to v34) and Safari always emit apopstate
event on page load, but Firefox doesn’t.”
We will need a library as a fallback to make the History Web APIs work consistently across the browser without any hurdles.
Meet History.js
Since the pushState()
method does not work to its full potential, in this tutorial we are going to leverage History.js. As the name implies, this JavaScript library is a polyfill, replicating the native History APIs that work across different browsers. It also exposes a set of methods similar to the native APIs, albeit with few differences.
As mentioned earlier, the browser native API is called through the history
window object with the lowercase “h”, while the History.js API is accessed through History
with the uppercase “H”. Given the previous example and assuming we have the history.js file loaded, we can revise the code, as follows (again, notice the uppercase “H”).
1 |
window.History.pushState( {}, title, url ); |
I hope this brief explanation is easy to understand. Otherwise, here are some further references if you want to learn more about the Web History API.
Building Our Static Website
In this section we won’t discuss each step needed to build a static website in detail. Our website is plain simple, as shown in the following screenshot:



You don’t have to create a website that looks exactly the same; you are free to add any content and create as many pages as you need. However, there are some particular points you need to consider regarding the HTML structure and the use of id
and class
attributes for some elements.
- Load jQuery and History.js within the document
head
. You may load it as a project dependency through Bower, or through a CDN like CDNJS or JSDelivr. - Wrap the header, the content, and footer in a
div
with the IDwrap
;<div id="wrap"></div>
- There are a few navigation items on the website header and the footer. Each menu should be pointing to a page. Make sure the pages exist and have content.
- Each menu link is given
page-link
class which we will use for selecting these menus. - Lastly, we give each link a
title
attribute which we’ll pass topushState()
to determine the document title.
Taking all this into account, our HTML markup will roughly look as follows:
1 |
<head>
|
2 |
<script src="jquery.js"></script> |
3 |
<script src="history.js"></script> |
4 |
</head>
|
5 |
<body>
|
6 |
<div id="wrap"> |
7 |
<header>
|
8 |
<nav>
|
9 |
<ul>
|
10 |
<li><a class="page-link" href="./" title="Acme">Home</a></li> |
11 |
<li><a class="page-link" href="./about.html" title="About Us">About</a></li> |
12 |
<!-- more menu -->
|
13 |
</ul>
|
14 |
</nav>
|
15 |
</header>
|
16 |
<div>
|
17 |
<!-- content is here -->
|
18 |
</div>
|
19 |
<footer>
|
20 |
<nav>
|
21 |
<ul>
|
22 |
<li><a href="tos.html" class="page-link" title="Terms of Service">Terms</a></li> |
23 |
<!-- more menu -->
|
24 |
</ul>
|
25 |
</nav>
|
26 |
<!-- this is the footer -->
|
27 |
</footer>
|
28 |
</div>
|
29 |
</body>
|
When you are done building your static website we can move on the main section of this tutorial.
Applying the History Web API
Before we begin writing any code, we need to create a new file to hold our JavaScript; we’ll name it script.js and load the file in the document before the body
closing tag.
Let’s add our first piece of code to change the document title and the URL upon clicking the menu navigation:
1 |
// 1.
|
2 |
var $wrap = $( "#wrap" ); |
3 |
|
4 |
// 2.
|
5 |
$wrap.on( "click", ".page-link", function( event ) { |
6 |
|
7 |
// 3.
|
8 |
event.preventDefault(); |
9 |
|
10 |
// 4.
|
11 |
if ( window.location === this.href ) { |
12 |
return; |
13 |
}
|
14 |
|
15 |
// 5.
|
16 |
var pageTitle = ( this.title ) ? this.title : this.textContent; |
17 |
pageTitle = ( this.getAttribute( "rel" ) === "home" ) ? pageTitle : pageTitle + " — Acme"; |
18 |
|
19 |
// 6.
|
20 |
History.pushState( null, pageTitle, this.href ); |
21 |
} ); |
I’ve split the code apart into several numbered sections. These will make it easier for you to pinpoint the code with the following reference:
- On the first line, we select the element,
<div id="wrap"></div>
, that wraps all of our website content. - We attach the click event. But, as you can see above, we attach it to the
#wrap
element instead of attaching the event directly on every menu navigation. This practice is known as event delegation. In other words, our#wrap
element is responsible for listening to click events on behalf of.page-link
. - We’ve also added
event.preventDefault()
so that the users will not be directed to the page in question. - If the clicked menu URL is the same as the current window we do not need to proceed to the next operation, simply because it is not necessary.
- The
pageTitle
variable contains the title format, derived from the link title attribute or the link text. Each page title follows{Page Title} — Acme
convention, except for the home page. “Acme” is our fictitious company name. - Lastly, we pass the
pageTitle
and the page URL to the History.jspushState()
method.
At this point, when we click on the menu navigation, the title as well as the URL should change accordingly as shown below:



Yet the page content remains the same! It is not updated to match the new title and the new URL.
Content
We need to add the following lines of code to replace the actual page content.
1 |
// 1.
|
2 |
History.Adapter.bind( window, "statechange", function() { |
3 |
|
4 |
// 2.
|
5 |
var state = History.getState(); |
6 |
|
7 |
// 3.
|
8 |
$.get( state.url, function( res ) { |
9 |
|
10 |
// 4.
|
11 |
$.each( $( res ), function( index, elem ) { |
12 |
if ( $wrap.selector !== "#" + elem.id ) { |
13 |
return; |
14 |
}
|
15 |
$wrap.html( $( elem ).html() ); |
16 |
} ); |
17 |
|
18 |
} ); |
19 |
} ); |
Again, the code here is split into several numbered sections.
- The first line of the code listens to the History change performed via the History.js
pushState()
method and runs the attached function. - We retrieve the state changes, containing various data like a URL, title, and id.
- Through the jQuery
.get()
method we retrieve the content from the given URL. - Lastly, we sort out the element with an
id
namedwrap
from the retrieved content, and eventually replace the current page content with it.
Once it’s added, the content should now be updated when we click on the menu navigation. As mentioned, we are also able to access visited pages back and forth through the browser Back and Forward buttons.



Our website is presentable at this point. However, we would like to step further by adding a little animation to bring the page to life and, finally, our website feels more compelling.
Adding Animation and Transitions
Animation in this situation need only be simple, so we’ll write everything fro scratch, instead of loading animations through a library like Animate.css, Motion UI of ZURB, or Effeckt.css. We’ll name the animation slideInUp
, as follows:
1 |
@keyframes slideInUp { |
2 |
from { |
3 |
transform: translate3d(0, 10px, 0); |
4 |
opacity: 0; |
5 |
}
|
6 |
to { |
7 |
transform: translate3d(0, 0, 0); |
8 |
opacity: 1; |
9 |
}
|
10 |
}
|
As the name implies, the animation will slide the page content from bottom to top along with the element opacity. Apply the animation to the element that wraps the page main content, as follows.
1 |
.section { |
2 |
animation-duration: .38s; |
3 |
animation-fill-mode: both; |
4 |
animation-name: slideInUp; |
5 |
}
|
The transition from one page to another one should now feel smoother once the animation is applied. Here, you may stop and call it a day! Our website is done and we are ready to deploy it for the world to see.
However, there is one more thing that you may need to consider adding, especially for those who want to monitor the number of visits and the visitors’ behavior on your website.
We need to add Google Analytics to track each page view.
Google Analytics
Since our pages will be loaded asynchronously (except for the initial page loaded) tracking the page view number should also be done asynchronously.
To begin with, make sure you have the standard Google Analytics added within the document head
. The code usually looks something as follows:
1 |
<script>
|
2 |
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ |
3 |
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), |
4 |
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) |
5 |
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); |
6 |
|
7 |
ga('create', 'UA-XXXXXX-XX', 'auto'); |
8 |
ga('send', 'pageview'); |
9 |
|
10 |
</script>
|
Then we need to adjust our JavaScript code to include the Google Analytics tracking code so that every page loaded asynchronously will also be measured as a page view.
We have several options. First, we can start counting when the user clicks a navigation link, or when changing the page title and URL, or when the content of the page has been fully loaded.
We’ll opt for the latter, which is arguably the most authentic, and in doing so we leverage the jQuery promise()
method after we change the page content, as follows:
1 |
$wrap.html( $( elem ).html() ) |
2 |
.promise() |
3 |
.done( function( res ) { |
4 |
|
5 |
// Make sure the new content is added, and the 'ga()' method is available.
|
6 |
if ( typeof ga === "function" && res.length !== 0 ) { |
7 |
ga('set', { |
8 |
page: window.location.pathname, |
9 |
title: state.title |
10 |
});
|
11 |
ga('send', 'pageview'); |
12 |
}
|
13 |
});
|
That’s all it is, we will now have the page view recorded in Google Analytics.



Wrapping Up
In this tutorial we have improved a simple static website with Web History API to make the page transition smoother, the load faster, and overall deliver a better experience to our users. At the end of this tutorial, we also implemented Google Analytics to record user page view asynchronously. Additionally, our website is perfectly crawl-able by search engine bots since it is, as mentioned, just a simple HTML website.
This was a meaty tutorial, explaining lots of things like CSS Animation, jQuery Ajax, and jQuery Promise. Here’s a handful of references for you to look into, to reinforce what you’ve learned.
- A Beginner's Introduction to CSS Animation
- Quick Tip: JavaScript Event Delegation in 4 Minutes
- AJAX for Front-End Designers
- Wrangle Async Tasks With jQuery Promises
- Demystifying Google Analytics
Lastly, don’t forget to visit the demo site of this tutorial as well as the source code in the repository.