Image for post

When you think of routers you usually think of libraries like React. But under the hood these libraries and frameworks still use vanilla JavaScript. So how do they do it? I hope this JavaScript router tutorial will help you understand how to put your own vanilla JS router together.

Creating your own router in vanilla JavaScript is relatively easy as long as you understand all of the separate parts involved in making one.

Key Parts: Here are the key things about making your own js router:

  1. Listening for “popstate” event for responding to .pathname changes. This happens whenever a new URL is typed into browser’s address bar but we don’t want to refresh the page simply refresh the view by loading new content.
  2. Basic understanding of history and and history.pushState (JavaScript History API) if you wish to integrate your router into native browser architecture.

A quick review of JavaScript History API

I’ve seen so many vanilla js router tutorials that don’t mention JavaScript History API. Too bad because clicking browser’s Back and Forward buttons has everything to do with navigating between URLs in browsing history. You can’t speak about routing without the History API.

  1. history.back() same as history.go(-1) or when user clicks Back button in their browser. You can use either method to the same effect.
  2. history.forward() executes when user will press browser’s Forward button and it is equivalent to history.go(1)
  3. go() is similar to .back() and forward() methods except you can specify how many steps back or forward you want to go within browser history stack.
  4. pushState() will push new state to history API.
  5. .length property is the number of elements in the session history.
  6. .state property is used to look up state without listening to “popstate” event.

Ok, let’s get started with our own vanilla js router implementation!

I’ll simply dump the minimum HTML, CSS and JavaScript loaded with comments. And then I’ll drop a GIF and GitHub link to source code.

History API-based Vanilla JS Router Setup

Let’s go over the very minimum code required to build a URL-switcher (without refreshing the page) and then I’ll show you a GIF of how it all works.

<script type = "module">
function select_tab(id) {
// Remove selected class from all buttons
item => item.classList.remove('selected'));
// select clicked element (visually)
document.querySelectorAll("#" + id).forEach(
item => item.classList.add('selected'));
function load_content(id) {
console.log("Loading content for {" + id + "}");
// Update text "Content loading for {id}..."
// Here you would do content loading magic...
// Perhaps run Fetch API to update resources

= 'Content loading for /' + id + '...';
function push(event) {
// Get id attribute of the button or link clicked
let id =;
// Visually select the clicked button/tab/box
// Update Title in Window's Tab
document.title = id;
// Load content for this tab/page
// Finally push state change to the address bar
window.history.pushState({id}, `${id}`,
window.onload = event => {
// Add history push() event when boxes are clicked
event => push(event))
event => push(event))
event => push(event))
event => push(event))
event => push(event))
// Listen for PopStateEvent
// (Back or Forward buttons are clicked)

window.addEventListener("popstate", event => {
// Grab the history state id
let stateId =;
// Show clicked id in console (just for fun)
console.log("stateId = ", stateId);
// Visually select the clicked button/tab/box
// Load content for this tab/page
* { /* global font */
font-family: Verdana;
font-size: 18px;
#root { display: flex; flex-direction: row; }
#content { display: flex;
display: block;
width: 800px;
height: 250px;
/* vertically centered text */
line-height: 250px;
border: 2px solid #555;
margin: 32px;
text-align: center;
.route {
cursor: pointer;
justify-content: center;
width: 150px;
height: 50px;
/* vertically centered text */
line-height: 50px;
position: relative;
border: 2px solid #555;
background: white;
text-align: center;
margin: 16px;
.route.selected { background: yellow; }


<section id = "root">
<section class = "route" id = "home">/home</section>
<section class = "route" id = "about">/about</section>
<section class = "route" id = "gallery">/gallery</section>
<section class = "route" id = "contact">/contact</section>
<section class = "route" id = "help">/help</section>

<main id = "content">Content loading...</main>



At the core is a call to window.history.pushState({id}, ${id}, /page/${id});

First parameter is a unique id of state. Second is Tab Title text. Finally, the third parameter is what you want your address bar to change to. Again, this is what makes the browser change URL without reloading the page.

The results. Now every time we click on a button the URL will actually change in browser’s address bar. The content box updates too.5

Image for post
Our vanilla JS router in action. Note that every time a button is clicked, history.pushState is triggered. We simply pass it the id of the clicked element stored in element’s id attribute: home, about, gallery, etc. They should coincide with the actual page you want to navigate to. Of course, this isn’t the only way to store page names, you can use an array[] for example, or any other way. It’s just the way it was done in this example.

You can fork it from my GitHub (router.html) in my vanilla js library.

Of course we also need to load content from the server referring to layouts and resources for that location. This is up to your application!

Making Back and Forward buttons work

Using history.pushState you will automatically make Back and Forward buttons navigate to a previous or next state. Doing that produces popstate event. This is the part where you must update your view once again. (The first time was when we clicked on the button.) But since the event carries an id of what was clicked it’s easy to update the view and reload content when Back or Forward are clicked:

We’re not using React or Vue here so in my source code load_content will take care of updating the view directly in DOM. This area is likely to be populated with some content loaded from your API. Since this is only front-end example there isn’t much I can do to show you how to do that. But that’s how it works on client-side.

Initial Router Load From The Server-Side

There is one more step required to put it all together. In my example I simply used router.html. When you load this router for the first time in a PWA, you have to make sure it works if, let’s say ./page/home was entered directly into address bar.

So far we’ve changed router address only from the front-end. It is assumed that every time you navigate to URL that appears on our router buttons it will actually be individually loaded from the server.

So it’s your responsibility to make sure ./page/about for example will load the router and the content associated ./page/about into root view of some sorts. (And also highlight the “current” button/tab.)

Once you implement that part your router will be complete. How you choose to reload content in #content element is entirely up to your back-end design.

Written by

Issues. Every webdev has them. Published author of CSS Visual Dictionary few others…

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store