Build a custom SPA Router using VanillaJS

Build a custom SPA Router using VanillaJS


8 min read


In this article, I will explain how I had built a custom SPA router using Vanilla JavaScript. I had to build a UI project without any using framework and had to figure out how to handle routing and discovered that you can build your own router using Vanilla JavaScript quiet easily.


I agree with the philosophy that we should not reinvent the wheel. And with the advent of frameworks such as ReactJS, Vue, each of them has its own custom routing library and I'll recommend you using them rather than building something ground up. However, the intent of this article is to explain that it is possible to write a custom router using VanillaJS and it also gives an opportunity to find out what happens under the hood.

Window - History & Location Objects

In order to build a custom router, we need to first understand the 'history' and the 'location' objects of the 'window' object and few methods that are required to handle the page navigation.

History Object

The window.history object provides the details regarding the browser's session history. It contains methods & properties that help you navigate back and forth through the user's history.

You can open your browser console and type history, and you'll see all the methods and properties of the history object listed as shown below.

Alt Text

Location Object

The window.location contains all the information related to the current location such as the origin, pathname, etc.

You can open your browser console and type location, and you'll see all the various properties and methods associated with the location object as shown below.

Alt Text

History - pushState()

The method pushState is used to add a state to the browser's session history stack.

Syntax: history.pushState(state, title, [, url]);

  • state - The JavaScript object associated with the new history entry. The state object can be anything that can be serialized.
  • title - The title is actually not used by Modern browsers yet. it is safe to pass an empty string or the title you wish you refer your state.
  • URL - The new history entry's URL is specified by this parameter.

We will be using the pushState method to update the browser's URL during page navigation.

Window - popstate event

The popstate event) is fired when the active history changes when the user navigates the session history.

In other words, whenever a back or a forward button is pressed on the browser, then the history changes and at that moment the 'popstate' event is fired.

We will be using the 'popstate' event to handle logic whenever the history changes.

Implementing the Router

Now that we've got the fundamentals in place, we will look at a step-by-step approach to implementing the router using VanillaJS.

The View

The index.html is a very simple page which contains an unordered list of links for the pages:

  • home
  • about
  • contact

In addition, there are 3 separate HTML for the home, about, and contact views.


<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vanilla JS Router</title>
    <ul class="navbar-list">
      <li class="navbar-item">
        <a href="#" onclick="onNavClick('/about'); return false;">About</a>
      <li class="navbar-item">
        <a href="#" onclick="onNavClick('/'); return false;">Home</a>
      <li class="navbar-item">
        <a href="#" onclick="onNavClick('/contact'); return false;">Contact</a>
    <div id="root"></div>
    <script src="./js/app.js"></script>


  <h1>******Welcome to the Home Page*****</h1>


  <h1>******Welcome to the About Page*****</h1>


  <h1>******Welcome to the Contact Page*****</h1>

Load the HTML pages (Async)

I have used the async/await with 'fetch API' for asynchronous loading of pages and have used 'promise' to assign the values to home, about and contact variables.

//Declare the variables for home, about & contact html pages
let home = '';
let about = '';
let contact = '';

 * @param {String} page - Represents the page information that needs to be retrieved
 * @returns {String} resHtml - The Page's HTML is returned from the async invocation

const loadPage = async (page) => {
  const response = await fetch(page);
  const resHtml = await response.text();
  return resHtml;

 * The Async function loads all HTML to the variables 'home', 'about' & 'contact'
const loadAllPages = async () => {
  home = await loadPage('home.html');
  about = await loadPage('about.html');
  contact = await loadPage('contact.html');

Let us walk through the flow for one page:

  • When the 'loadAllPages' function is invoked, the first function loadPage('home.html') first fired.
  • Inside the 'loadPage' function, the fetch('home.html') will be fired to load the home.html asynchronously.
  • The 'await' keyword ensures that the 'response' variable is populated and the 'resHtml' is assigned 'response.text()' since the text is returned in the API call.
  • The value of 'resHtml' is returned to the 'loadAllPages' function and assigned to the 'home' variable.

Likewise, API calls are made for 'about' and 'contact' pages as well and the values are populated to the variables about & contact.

The Main Function & Root Element

Fetch the 'rootDiv' from the 'index.html' document.

The main function will be invoked on the Page load. Inside, the main function, we are first ensuring that all the HTML pages are loaded into the variables 'home', 'about' and 'contact'.

In order to ensure that the 'home' page is loaded to the root element upon page load, the rootDiv.innerHTML is set to 'home' variable.

Further, the 'routes' are set up with the corresponding page mapping in order to load the appropriate page when the routes are called.

//Get the Element with the Id 'root'
const rootDiv = document.getElementById('root');

 * The Main Function is an async function that first loads All Page HTML to the variables
 * Once the variables are loaded with the contents, then they are assigned to the 'routes' variable
const main = async () => {
  await loadAllPages();
  rootDiv.innerHTML = home;
  routes = {
    '/': home,
    '/contact': contact,
    '/about': about,

// Invoke the Main function

From the above index.html, we are invoking the 'onNavClick' method and passing in the 'route' upon clicking the 'a' link as shown in the code snippet below.

<li class="navbar-item">
    <a href="#" onclick="onNavClick('/about'); return false;">About</a>
 * @param {String} pathname - Pass the 'pathname' passed from onClick function of the link (index.html)
 * The function is invoked when any link is clicked in the HTML.
 * The onClick event on the HTML invokes the onNavClick & passes the pathname as param
const onNavClick = (pathname) => {
  window.history.pushState({}, pathname, window.location.origin + pathname);
  rootDiv.innerHTML = routes[pathname];

The onNavClick method accepts the 'pathname' which is the 'route' link and uses the window.history.'pushState' method to alter the state.

The second line 'rootDiv.innerHTML = routes[pathname]' will render the appropriate page based on what is configured within the routes in the main function (see above).

At this point, you have a functional router that navigates to the appropriate page upon clicking a link and the corresponding link is also updated in the URL browser.

The only thing that you'll notice is that when you hit a 'back' or 'forward' button on the browser, the links are correctly updated on the URL, however, the contents on the page are not refreshed.

Let us take care of that in the last section of the article.

Handle Page Rendering upon State Change

If you would recall from the above definition of 'onpopstate event' method, it'll be invoked whenever the active history changes in the browser.

We are using that hook to ensure that the rootDiv is populated with the appropriate page based on the routes configured.

That's it!! You should now have a fully functional custom router built using Vanilla JavaScript.

 * The Function is invoked when the window.history changes
window.onpopstate = () => {  
  rootDiv.innerHTML = routes[window.location.pathname];

If you would like the complete code, you can find it on Github over here.


To summarize, we've covered how to build a basic custom router using VanillaJS. The router uses the window's history and location objects primarily and the methods pushState & onpopstate event.

Hope you enjoyed this article. Don't forget to subscribe and connect with me on Twitter @skaytech

Cover Photo by Nick Fewings on Unsplash

You might also enjoy the following articles:

Did you find this article valuable?

Support Skay by becoming a sponsor. Any amount is appreciated!