cf.completefrontendCode editorOpen lab
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
    Inspect a URL safelyRead location-like parts without changing the real page.
  • 02
    Model browser historyExplain entries, the current pointer, forward truncation, state objects, and popstate.
  • 03
    Pick a routing styleCompare normal links, hash routes, History API routes, and the Navigation API.

The central idea

A browser tab remembers where it has been. That memory is called the session history: a list of entries plus a current pointer. When you press Back, the browser usually moves that pointer to an older entry. When you press Forward, it moves to a newer one.

Modern web apps use that same memory to feel like many pages while staying inside one loaded document. A router can show /inbox, then /inbox/42, then return to /inbox when Back is pressed. The trick is not magic: read the current URL, decide what route it means, render the matching screen, and keep history in sync.

Safety rule for this lesson

The experiments never call history.pushState, never call history.replaceState, never assign to location or location.hash, and never call navigation.navigate on this lesson page. The Next.js router owns the real address here, so your Back button keeps working. Demos use tested in-page models plus read-only browser facts after mount.

Real-life analogySession history is a breadcrumb trail

Imagine dropping breadcrumbs on a trail. Back and Forward do not create crumbs; they move you along crumbs that already exist. If you walk back and then choose a new path, the old forward crumbs no longer belong to your trail.

In real life: Breadcrumbs on the path
In JavaScript: History entries in this tab
In real life: Where you stand now
In JavaScript: The current index or pointer
In real life: Walk backward or forward
In JavaScript: Back and Forward traverse entries
In real life: Start a new branch
In JavaScript: A new push after Back drops forward entries

Where the analogy stops: Real breadcrumbs do not store JavaScript state objects. Browser entries can hold a structured-cloned state value as well as a URL.

We will use ideas from URL & URLSearchParams, Introduction to events, Default actions, and Event delegation.

Read the current address with location

INTERACTIVE

The window.location object describes the current document address. You can read href, origin, pathname, search, and hash. Reading is harmless. Assigning to many location properties navigates, so our inspector only reads the real page after mount and uses new URL() for the editable address.

Location inspector: real page, editable address
Location-shaped codePop 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);
Read-only resultsafter mount

Current page

Reading browser location…

history.length: reading…

Editable URL with base

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 left panel reads this lesson page after mount. The right panel parses your input with new URL(input, base), so experimenting never changes the real address or Back button.

Reading location is safe. Assigning to location, location.hash, or calling router navigation would navigate, so this lesson does not do that.

Treat pathname as the clean route path, search as small shareable state like filters, and hash as the fragment after #. The hash can identify a heading or act as a client-side route, but it is not sent to the server during normal navigation.

Session history as entries plus a pointer

INTERACTIVE

A tab’s history is not just a stack you can push and pop. It is more like an array with a current index. Back decreases the index. Forward increases it. A new visit from the middle keeps entries up to the pointer and discards the future branch.

History stack simulator
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;}
Model stackindex 0
  1. Current /home document state: {"page":"home"}

loaded /home

Try it yourself

Current entry: /home. The log says whether a traversal fired popstate; pushState and replaceState do not.

A faithful model of entries, index movement, state cloning, and forward truncation. It never touches this tab's real session history.

Try this sequence: pushState, User clicks link, Back, then pushState. The old forward entry disappears. Notice the log too: adding or replacing an entry does not log popstate; traversal does.

pushState, replaceState, and popstate

STEP THROUGH

The History API lets code add or edit same-origin entries. The signatures are history.pushState(state, "", url) and history.replaceState(state, "", url). The second argument exists for historical reasons; pass an empty string. The URL must be same-origin. The state value is structured-cloned, so keep it small and serializable.

Real-life analogypushState changes the signpost without walking

Adding a crumb and changing the sign does not mean you walked back or forward. That is why pushState and replaceState do not fire popstate. The bell rings when the traveler moves along the trail.

In real life: Add a breadcrumb
In JavaScript: pushState creates a new entry
In real life: Rewrite the sign on this spot
In JavaScript: replaceState edits the current entry
In real life: A bell rings when the traveler moves
In JavaScript: popstate fires on traversal

Where the analogy stops: A signpost does not enforce web security. Browsers do: the URL passed to pushState or replaceState must stay on the same origin.

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

Choose a router style, predict the render order, then step through the model. It records the same sequence a browser router uses without touching this lesson page's real history.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  "/": "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: "/" } });
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Router style

Changing the router starts a fresh replay. The replay is a model, not a live mutation of 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.

A router usually renders immediately after its own pushState call. Later, a popstate listener renders from event.state. Same-document fragment navigations can also participate in traversal and fire popstate in modern browsers; hashchange is the fragment-specific event.

Hash routing: sticky tabs inside one book

INTERACTIVE
Real-life analogyHash routes are sticky tabs inside one book

A hash route puts app state after #. The server sees the book path, while the browser keeps the sticky tab. That is why hash routes are less pretty but very easy to deploy on static hosts.

In real life: The book itself
In JavaScript: The loaded document, such as /app
In real life: A sticky tab
In JavaScript: The fragment, such as #/products
In real life: Moving to another tab
In JavaScript: A same-document hash navigation

Where the analogy stops: A sticky tab is visible to a person holding the book. A URL fragment is not sent to the server in a normal request; client code reads it after the page loads.

Mini router lab: path routes vs 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");
Rendered routehistory
Address shapehttps://site.example/products
ScreenProducts
Direct visit

Needs a rewrite to index.html

Try it yourself

This clean path is a great app URL, but a direct visit needs the host to rewrite it to the app shell.

Both modes render the same screens. Deployment is the difference: History routes need rewrite support for deep links; hash routes usually do not.
History API routing vs hash routing
QuestionHistory API routeHash route
What changes?The path/search, such as /products?page=2The fragment, such as #/products?page=2
Sent to server?Yes on reload or direct visitNo. The part after # stays in the browser
Deep-link requirementServer/host must rewrite app paths to index.htmlWorks on almost any static host
Events to listen forClicks plus popstateClicks plus hashchange and traversal

Use History API routes when your host can rewrite unknown app paths to the app shell. Use hash routes when you cannot configure the host and need direct links to work anywhere. Either way, keep normal links for real document navigations.

Where you will use this

SORTER

Routers sit at the boundary between browser behavior and app behavior. You will use these rules when building dashboards, product filters, documentation sites, modal routes, tabbed interfaces, and login flows. The professional question is not “Can JavaScript change the URL?” It is “Should this be a real document load, a clean client route, or a fragment inside one document?”

Choose the navigation tool
  • 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 static files with no rewrite rules
  • Jump to #comments in a long article
Try it yourself
0 of 6 correct

Sort each need into the simplest reliable navigation style.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Practical router checklist
  • Use real <a href> values so links can be copied and opened in new tabs.
  • Cancel the default action only for same-origin routes your app will render.
  • Render after your own push, and render from event.state on traversal.
  • Configure server rewrites before shipping History API deep links.

Common misconceptions

“Reading location navigates.”

Reading is safe. Assigning to location.href or location.hash can navigate.

“pushState fires popstate.”

It does not. Render immediately after pushing. Listen for popstate when the user traverses history.

“replaceState adds a new Back-button step.”

It edits the current entry, which is perfect for cleanup like replacing ?modal=signup after the modal closes.

“The server sees hash routes.”

The fragment after # stays in the browser. That is useful for static hosting and limiting for server-side handling.

“Clean paths always work on refresh.”

Only if the server or host rewrites app paths to the app shell. Without that, a direct visit can be a 404.

“The Navigation API replaces all routing code today.”

It is promising, but not universal. Feature detection and fallbacks are still part of the design.

Practice: history and routing

5 EXERCISES
Exercise 1 · Warm-upPredict the branch

Predict the first line printed by this tiny stack model.

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?

    Decide whether a same-origin path stays on the same origin.

    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 from the URL parser.

      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 · PracticeRender from event.state

        Predict what a Back-button handler renders.

        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 shipping a static documentation app. Write down which route shape you would choose with rewrite support and without rewrite support, and explain why.

            Quiz: check your understanding

            7 QUESTIONS

            Read each explanation, even when your first answer is right. Router bugs often come from mixing up adding entries, replacing entries, and traversing entries.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What does this forward-truncation model 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 7When does popstate fire for History API entries?

              Choose an answer to see the explanation.

            3. Question 3 of 7What same-origin check does this print first?

              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 7Which statement about hash routes is accurate?

              Choose an answer to see the explanation.

            5. Question 5 of 7What does this hash parsing snippet print second?

              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 7Why does a History API app need server rewrite rules for deep links?

              Choose an answer to see the explanation.

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

              Choose an answer to see the explanation.

            Key takeaways

            • location exposes the current URL parts; reading them is safe.
            • Session history is entries plus a current pointer; new pushes from the middle drop forward entries.
            • pushState and replaceState require same-origin URLs and do not fire popstate.
            • popstate is for traversal; hashchange is for fragment changes.
            • Hash routes work almost anywhere; History API routes need server rewrite support for deep links.
            • The Navigation API is a progressive enhancement: feature-detect it and keep fallbacks.

            Remember the one-liner.
            Navigation is either loading a new document or moving the current document’s history pointer; routers make pointer movement render like pages.

            Up next: Windows, iframes & postMessage.

            CompleteFrontend Clear concepts. Working examples.