completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

History & navigation

Change the URL without reloading, inspect location parts, and understand how History API, hash routing, popstate, and the Navigation API power client-side routers.

What you’ll learn
  • 01
    Read an address safelyRead the parts of location, like pathname and hash, without changing the real page.
  • 02
    Picture the history listExplain the list of entries, the pointer to the current one, why entries ahead get dropped, the saved state, and when popstate fires.
  • 03
    Pick a kind of routeCompare normal links, hash routes, History API routes, and the Navigation API.

The central idea

A browser tab remembers the pages you visited in it. This memory is called the session history. It has two parts:

  • a list of entries, one for each place you visited in this tab;
  • a pointer that marks which entry is the current one.

When you press Back, the browser moves the pointer to the entry before. When you press Forward, it moves the pointer to the entry after.

Many web apps load one page and then change what it shows, without loading a new page. This is called client-side routing. The code that does it is called a router. A router can show /inbox, then /inbox/42, and go back to /inbox when you press Back. A router does four jobs:

  1. It reads the current address (the URL).
  2. It works out which screen that address stands for.
  3. It shows that screen.
  4. It keeps the session history up to date.
Safety rule for this lesson

The demos on this page never change the real address or your real history. They never call history.pushState, history.replaceState, or navigation.navigate. They never set location or location.hash. This site's own router (from Next.js) controls this page's address, so your Back button keeps working. The demos use small, tested pretend versions instead. They only read real browser values after the page has loaded.

Real-life analogySession history is a breadcrumb trail

Imagine you drop a breadcrumb at each step on a forest path. Walking back or forward drops no new crumbs. You only move along the crumbs that are already there. Now say you walk back a few steps and then take a different path. The crumbs ahead of you on the old path are no longer part of your trip.

In real life: The breadcrumbs on the path
In JavaScript: The entries in this tab's history
In real life: The crumb where you stand
In JavaScript: The current entry, marked by the pointer
In real life: Walking back or forward along the crumbs
In JavaScript: Pressing Back or Forward
In real life: Walking back, then taking a new path
In JavaScript: A new entry after Back drops the entries ahead

This lesson uses ideas from these earlier lessons: URL & URLSearchParams, Introduction to events, Default actions, and Event delegation.

Read the current address with location

INTERACTIVE

window.location is an object that describes the address of the current page. You can read these parts of it:

  • href: the whole address.
  • origin: the scheme, host, and port, like https://shop.example. Two addresses with the same origin belong to the same site.
  • pathname: the path, like /app/products.
  • search: the part from ? on, like ?sort=popular.
  • hash: the part from # on, like #details.

Reading these parts is safe. Setting many of them makes the browser go to a new address. So the inspector below only reads this page's real address, after the page has loaded. For the address you type, it uses new URL(input, base). That builds a separate URL object with the same parts, and it changes nothing on the page.

Location inspector: this page, and an address you type
Read each part of an addressPop out in the code editor (opens in a new tab)JavaScript
const url = new URL(input, base);console.log(url.href);console.log(url.origin);console.log(url.pathname);console.log(url.search);console.log(url.hash);
Parts it readread only

This page

Reading this page's address…

history.length: reading…

The address you typed, read against

https://shop.example/app/catalog/

hrefhttps://shop.example/app/products?sort=price#reviews
originhttps://shop.example
pathname/app/products
search?sort=price
hash#reviews
Try it yourself

The first list reads this lesson page's real address, after the page has loaded. The second list reads the address you type, with new URL(input, base). Nothing here changes the real address or your Back button.

Reading location is safe. Setting location or location.hash would make the browser go somewhere, so this lesson never does that.

What each part is for:

  • pathname is the route. It is the main part a router looks at.
  • search holds small settings you want to share in a link, like filters.
  • hash is the part after #. It can point to a heading on the page, or it can hold a route. The browser does not send it to the server when it loads a page.

The table below shows four ways to change the address with location. Read each row as: when I write this, this is what happens, and this is what Back does next.

Four ways to change the address with location
When you writeWhat happensWhat Back does next
location.href = urlThe browser loads a new page.Back returns to the old page.
location.assign(url)The browser loads a new page.Back returns to the old page.
location.replace(url)The browser loads a new page in place of the current entry.Back skips the old page.
location.hash = valueUsually the page stays, and only the hash changes.Back may return to the old hash.
Three ways to load a new pageJavaScript
const next = "/checkout"; // In a browser, each line below loads a new page.location.href = next;location.assign(next);location.replace(next);

Line 4 and line 5 both load the new page and keep the old page in the history list, so Back can return to it. Line 6, location.replace(next), loads the new page too. But the new page takes the place of the current entry, so Back skips the old page. That is useful after a one-time step, such as a finished sign-in.

Setting only location.hash usually stays on the same page. It can add a new entry for the new hash.

Session history: a list of entries and a pointer

INTERACTIVE

You can picture a tab's history as an array of entries plus a number, index. The number points at the current entry, so we call it the pointer.

  • Back makes the pointer 1 smaller.
  • Forward makes the pointer 1 bigger.
  • A new entry added while the pointer is in the middle first removes every entry ahead of the pointer. Then it goes at the end.

The simulator below is a pretend history list. Its buttons change only the list in the simulator, not your real history.

History list simulator
A history list in a few linesPop out in the code editor (opens in a new tab)JavaScript
const stack = ["/home"];let index = 0;function push(url) {  stack.splice(index + 1);  stack.push(url);  index = stack.length - 1;}function back() {  if (index > 0) index = index - 1;}
Entriespointer at 0
  1. Current /home document state: {"page":"home"}

loaded /home

Try it yourself

Current entry: /home. The log says when popstate fires. pushState and replaceState never fire it.

A pretend history list: its entries, the pointer, the saved state, and how a new entry drops the entries ahead. It never touches this tab's real history.

Press these buttons in order: pushState, User clicks link, Back, then pushState. The /about entry disappears. Also watch the log. Adding or replacing an entry never logs popstate. Moving with Back, Forward, or go(-2) does.

Before: the list is /home, /cart?from=nav, /about, and the current entry is /cart?from=nav. After pushState: /about is dropped and the list is /home, /cart?from=nav, /cart?from=nav, with the new entry current.Before: Back put you on entry 1/home/cart?from=nav/aboutcurrentdroppedAfter pushState/home/cart?from=nav/cart?from=navnew, current
Back put you on entry 1, so /about was ahead of you. pushState drops /about and adds the new entry in its place.

pushState, replaceState, and popstate

STEP THROUGH

The History API is the set of tools on history that let your code add or change entries in this tab's list. Two of them change entries:

  • history.pushState(state, "", url) adds a new entry and makes it the current one.
  • history.replaceState(state, "", url) changes the current entry instead of adding one.

Each one takes three values:

  1. state is a small piece of data saved with the entry, like { route: "/products" }.
  2. "" is an old value that browsers no longer use. Always pass an empty string.
  3. url is the new address. It must have the same origin as the current page.

Neither one loads a page. The address bar changes, and the page stays as it is.

Back, Forward, and history.go() move the pointer to an entry that already exists. When that happens, the browser fires a popstate event. Its event.state is the state saved with that entry.

Real-life analogypushState drops a crumb without walking

Dropping a new crumb is not walking. Swapping the crumb under your feet is not walking either. The bell only rings when you walk back or forward along the crumbs. In the same way, pushState and replaceState never fire popstate.

In real life: Drop a new breadcrumb
In JavaScript: pushState adds a new entry
In real life: Swap the crumb you stand on
In JavaScript: replaceState changes the current entry
In real life: A bell rings each time you walk to another crumb
In JavaScript: popstate fires when the pointer moves

The browser saves a copy of state, not the object you passed. The code below shows this with structuredClone(). It is a built-in function that makes a deep copy: a copy of the object and of every object inside it. The History API copies state in the same way.

The saved state is a copyPop out in the code editor (opens in a new tab)JavaScript
const state = { page: "cart", filters: { color: "blue" } };const saved = structuredClone(state);state.filters.color = "red";console.log(saved.filters.color);console.log(state.filters.color);console.log(saved === state);

In plain words, line by line:

  1. Line 1 makes state, with an inner filters object.
  2. Line 2 copies it into saved.
  3. Line 3 changes the color in state to "red".
  4. Line 4 prints blue. The copy did not change.
  5. Line 5 prints red.
  6. Line 6 prints false, because saved and state are two different objects.
structuredClone copies the state object into saved while color is blue. Later, line 3 changes state to color red. The saved copy still has color blue.color: "red"statechanged laterstructuredClonecolor: "blue"savedstill blue
structuredClone made the copy while the color was still "blue". Line 3 changes state to "red", and the copy stays "blue".

Some values can't be copied this way: functions, and DOM nodes (the objects for elements on the page). Browsers may also limit how big the state can be. So keep the state small, like a route name or a filter object. Put bigger data in storage, such as sessionStorage, and save only its key in the state.

The table below shows what you can put in state. Read each row as: this kind of value, can it go in, and what to do.

What you can put in a history state object
What you put inCan it go in?What to do
Plain objects, arrays, strings, and numbersYes.Save only the small data you need to show the screen.
Functions and DOM nodesNo.Save an id or a string instead.
Large dataMaybe not.Put it in sessionStorage or your app's data, and save its key.
The object you passed to pushStateOnly a copy goes in.Changing your object later does not change history.state.

A router uses these tools at two moments: when the user clicks a link, and when the user presses Back. The step-through below follows both. In the History API version of its code, console.log("pushState " + href) stands in for the real history.pushState call, so this page's address doesn't change.

Step through a tiny router
Step 0 of 11Ready
Your turn: follow the blue line

Choose a router style. Guess the order of the printed lines, then step through. This is a pretend model: it never touches this page's real history.

Running in
  1. script
Next: line 1
  "/": "Home page",  "/products": "Products page",};let url = "/";let state = { route: "/" };function render(route) {  console.log(routes[route] ?? "Not found");}function clickLink(href) {  event.preventDefault();  state = { route: href };  url = href;  console.log("pushState " + href);  render(state.route);}function onPopState(event) {  state = event.state;  render(state.route);}clickLink("/products");onPopState({ state: { route: "/" } });
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Router style

Choosing another router style starts the replay again. The replay is a pretend model; it never changes this page.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

After its own pushState call, the router shows the new screen right away, because no event will come. Later, when the user presses Back, the popstate listener shows the screen named in event.state.

Left: pushState adds the entry /products after /. No popstate event fires. Right: the user presses Back and the current entry moves from /products to /. A popstate event fires.pushState adds an entry//productsnew entryno popstateBack moves to an entry//productspopstate fires
pushState adds an entry and fires no event, so the router shows the new page itself. Back moves to an entry that already exists, and that fires popstate.

In modern browsers, moving Back or Forward between two hashes on the same page fires popstate too. There is also a separate event just for hash changes, called hashchange.

The scroll position is saved separately

When you press Back or Forward, browsers usually scroll the page back to where it was. history.scrollRestoration controls this. It is "auto" by default, which means the browser restores the scroll position. A router can set it to "manual" and restore the position itself. Then the router must also move the scroll and the keyboard focus to the right place. Keep "auto" unless your app has a clear plan for this.

Read and change who restores the scrollJavaScript
console.log(history.scrollRestoration);history.scrollRestoration = "manual";console.log(history.scrollRestoration);

In a browser, line 1 prints auto, the default. Line 2 switches to manual. Line 3 prints manual.

Hash routing: the route after #

INTERACTIVE

A hash route keeps the route in the hash, the part of the address after #. For example, in https://site.example/app#/products, the page is /app and the route is #/products.

Real-life analogyHash routes are sticky tabs inside one book

A book has sticky tabs that mark its parts. To reach another part, you open the same book at another tab. You don't fetch a new book. The server only hands over the book: /app. The browser keeps the sticky tab: #/products.

In real life: The book itself
In JavaScript: The loaded page, such as /app
In real life: A sticky tab inside the book
In JavaScript: The hash, such as #/products
In real life: Opening the book at another tab
In JavaScript: Changing only the hash, on the same page

Hash addresses look less tidy than clean paths. But they work on almost any host that serves plain files, with no extra setup. The lab below shows the same screens with both kinds of route.

Mini router lab: path routes and hash routes
Two route tablesPop out in the code editor (opens in a new tab)JavaScript
const historyRoutes = { "/": "Home", "/products": "Products" };const hashRoutes = { "#/": "Home", "#/products": "Products" };function render(mode, route) {  const table = mode === "history" ? historyRoutes : hashRoutes;  return table[route] ?? "Not found";}render("history", "/products");render("hash", "#/products");
Screen it showshistory
Addresshttps://site.example/products
ScreenProducts
Direct visit

Needs a rewrite to index.html

Try it yourself

This clean path is a good app address. But a direct visit only works if the host sends it to the app's main page (a rewrite).

Both kinds show the same screens. The difference is hosting: History routes need a rewrite for direct visits; hash routes usually don't.

The table below compares the two ways to keep a route in the address. Read each row as a question, with one answer for each kind of route.

History API routes and hash routes, side by side
QuestionHistory API routeHash route
What part of the address changes?The path and search, like /products?page=2.The hash, like #/products?page=2.
Does the server get the route?Yes, after a refresh or a direct visit.No. The part after # stays in the browser.
What does a direct visit need?The host must send app paths to index.html (a rewrite).Nothing extra. It works on almost any host of plain files.
Which events do you listen for?Link clicks and popstate.Link clicks, hashchange, and moves with Back or Forward.

The code below splits one address into its path and its hash.

The hash is not part of the pathPop out in the code editor (opens in a new tab)JavaScript
const url = new URL("https://site.example/app#/products?tag=js");console.log(url.pathname);console.log(url.hash);

Line 2 prints /app, because the path stops at #. Line 3 prints #/products?tag=js, the whole hash with its #. The ?tag=js here is part of the hash, not the search part.

The browser has the address /app#/products?tag=js. It asks the server for /app only. The part #/products?tag=js stays in the browser.browseraddress/app#/products?tag=js#/products?tag=jsstays in the browserasks forservergets/app/appis all it gets
The browser asks the server for /app only. Everything from # onward stays in the browser, where your router code reads it.

Which kind of route to use:

  • Use History API routes, like /products, when you can set up your host to send every app path to the app's main page, index.html. This host setting is called a rewrite.
  • Use hash routes, like #/products, when you can't change the host's settings and links must still work everywhere.
  • In both cases, keep normal links for going to a real new page.

Changing the hash for real can fire hashchange. Changing the address with pushState or replaceState does not, even when the new address has a different hash. So pick one event to listen for, and update the screen only from that event. Otherwise the app may draw the same screen twice.

Where you will use this

SORTER

A router sits between what the browser does and what your app does. You will use these rules in dashboards, product filters, documentation sites, pop-ups that have their own address, tabbed screens, and sign-in steps.

For each move, ask one question. Should it load a new page, change a clean path with the History API, or change only the hash on the same page?

Pick the right way to move
  • Open https://docs.example
  • Go from /pricing to /contact on a simple site
  • Switch /inbox to /inbox/42 inside a dashboard
  • Save /products?color=blue after a filter click
  • Host a tiny app on plain files with no rewrite rules
  • Jump to #comments in a long article
Try it yourself
0 of 6 correct

Put each need under the simplest way that works for it.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
A checklist for a router
  • Give every link a real address in <a href>, so people can copy it or open it in a new tab.
  • Cancel a click's default action with preventDefault() only for links on your own site that your app will show itself.
  • After your own pushState, show the new screen right away. When popstate fires, show the screen from event.state.
  • Set up rewrites on your host before you ship History API routes, so direct visits work.

Common misconceptions

“Reading location makes the page move.”

Reading is safe. Setting location.href or location.hash can make the browser go to a new address.

“pushState fires popstate.”

It does not. Show the new screen right after you call pushState. Listen for popstate to know when the user presses Back or Forward.

“replaceState adds a new step for the Back button.”

It changes the current entry. That is handy for cleanup. For example, after a sign-up pop-up closes, you can replace ?modal=signup in the address.

“The server sees hash routes.”

The part after # stays in the browser. That makes hash routes easy to host as plain files. It also means the server can't use the route.

“Clean paths always work after a refresh.”

Only if the host sends app paths to the app's main page (a rewrite). Without that, a direct visit can show a 404 "not found" page.

“The Navigation API replaces all routing code today.”

It is useful, but not every browser has it. You still need to check for it and keep a fallback.

Practice: history and routing

5 EXERCISES
Exercise 1 · Warm-upPredict what is left after Back

Predict the first line this small history model prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const stack = ["/home"];
let index = 0;
stack.push("/shop");
index = 1;
index = index - 1;
stack.splice(index + 1);
stack.push("/help");
console.log(stack.join(" -> "));
console.log(index);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeSame origin or not?

    Does a path that starts with / stay on the same origin? Predict the first line.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const current = new URL("https://app.example/home");
    const next = new URL("/cart", current.origin);
    console.log(next.origin === current.origin);
    console.log(next.pathname);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the hash

      Predict the second line this code prints.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const url = new URL("https://example.com/app#/products?tag=js");
      console.log(url.pathname);
      console.log(url.hash);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeShow the screen from event.state

        Predict what a Back-button handler shows.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const routes = { "/": "Home", "/settings": "Settings" };
        const event = { state: { route: "/settings" } };
        console.log(routes[event.state.route]);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePlan a tiny docs router

          You are building a documentation app made of plain files. Which kind of route would you pick if your host supports rewrites? Which would you pick if it doesn't? Write down both, and say why.

            Quiz: check your understanding

            9 QUESTIONS

            Read each explanation, even when your first answer is right. Many router bugs come from mixing up three things: adding an entry, replacing an entry, and moving between entries.

            Lesson quiz · 9 questionsScore: first tries count
            1. Question 1 of 9This small history model goes back, then adds a new entry. What does the first line print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const stack = ["/home"];
              let index = 0;
              stack.push("/shop");
              index = 1;
              index = index - 1;
              stack.splice(index + 1);
              stack.push("/help");
              console.log(stack.join(" -> "));
              console.log(index);

              Choose an answer to see the explanation.

            2. Question 2 of 9When does popstate fire for entries made with the History API?

              Choose an answer to see the explanation.

            3. Question 3 of 9This code compares two origins. What does the first line print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const current = new URL("https://app.example/home");
              const next = new URL("/cart", current.origin);
              console.log(next.origin === current.origin);
              console.log(next.pathname);

              Choose an answer to see the explanation.

            4. Question 4 of 9Which sentence about hash routes is true?

              Choose an answer to see the explanation.

            5. Question 5 of 9This code splits an address into parts. What does the second line print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const url = new URL("https://example.com/app#/products?tag=js");
              console.log(url.pathname);
              console.log(url.hash);

              Choose an answer to see the explanation.

            6. Question 6 of 9Why does an app with History API routes need a rewrite on the host for direct visits?

              Choose an answer to see the explanation.

            7. Question 7 of 9What is the main idea of the Navigation API?

              Choose an answer to see the explanation.

            8. Question 8 of 9Which one loads a new page in place of the current history entry?

              Choose an answer to see the explanation.

            9. Question 9 of 9This code copies an object, then changes the original. What does the first line print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const state = { page: "cart", filters: { color: "blue" } };
              const saved = structuredClone(state);
              state.filters.color = "red";
              console.log(saved.filters.color);
              console.log(state.filters.color);
              console.log(saved === state);

              Choose an answer to see the explanation.

            Key takeaways

            • location holds the parts of the current address. Reading them is safe.
            • Session history is a list of entries plus a pointer to the current one. A new entry added from the middle drops the entries ahead.
            • pushState and replaceState need an address with the same origin. They never fire popstate.
            • popstate fires when Back, Forward, or go() moves the pointer. hashchange fires when the hash changes.
            • Hash routes work on almost any host. History API routes need a rewrite on the host so direct visits work.
            • Not every browser has the Navigation API. Check for it, and keep a fallback.

            Remember the one-liner.
            Going somewhere means one of two things: loading a new page, or moving the current page's history pointer. A router makes pointer moves look like new pages.

            Up next: Windows, iframes & postMessage.

            CompleteFrontend Clear concepts. Working examples.