Complete JavaScript Course Module 10: Intermediate: The Browser Object Model (BOM)

JavaScript History API: back, forward and pushState

  • Beginner
  • Lesson 10.3
  • 6 min read
  • 43 of 76 in the course

In short

The JavaScript History API lets you move through the browser's back and forward history and change the URL without reloading the page. You will learn history.back, forward and go, pushState, replaceState and the popstate event. What is the history object?

The JavaScript History API lets you move through the browser's back and forward history and change the URL without reloading the page. You will learn history.back, forward and go, pushState, replaceState and the popstate event.

What is the history object?

Every browser tab keeps a list of the pages you visited in it, like a stack of cards. The Back button moves to the previous card and Forward moves to the next. window.history lets JavaScript use that same list.

For privacy, you cannot read the addresses in the list. You can only move through it and add or change entries for your own site. history.length tells you how many entries the tab has.

How to go back and forward with history.back, forward and go

These three methods act exactly like the browser's buttons:

history.back();     // same as clicking Back
history.forward();  // same as clicking Forward
history.go(-2);     // go back two pages
history.go(1);      // go forward one page
history.go(0);      // reload the current page

console.log(history.length);
// Output (example): 4

If there is no page to go back to, nothing happens and no error is thrown. A common use is a "Back" link inside a page:

<!DOCTYPE html>
<html lang="en">
<body>
  <a href="/products" id="backLink">Back to products</a>

  <script>
    const backLink = document.querySelector("#backLink");

    backLink.addEventListener("click", (event) => {
      // If the user came from another page in this tab, go back to it.
      if (history.length > 1) {
        event.preventDefault();
        history.back();
      }
      // Otherwise the normal link to /products works.
    });
  </script>
</body>
</html>

If the user arrived from a product list, clicking the link returns them to it, with their old scroll position. If they opened the page directly from a bookmark, history.length is 1, so the normal href takes them to /products instead.

Note: history.length > 1 is only a rough check: the earlier entry might be another website. For most simple sites it is good enough, and the href fallback keeps the link useful either way.

How to change the URL without reloading: pushState and replaceState

Modern web apps often show new content without loading a whole new page. But users still expect the URL to change, so they can bookmark it and use the Back button. That is what pushState and replaceState do.

// history.pushState(state, unused, url)
history.pushState({ tab: "reviews" }, "", "?tab=reviews");
console.log(location.search);   // Output: ?tab=reviews
console.log(history.state);     // Output: { tab: "reviews" }

history.replaceState({ tab: "specs" }, "", "?tab=specs");
console.log(location.search);   // Output: ?tab=specs

Run this in the console and watch the address bar: the URL changes, but the page does not reload. The three arguments are:

  1. state: any small piece of data you want to remember for this entry (an object, string or number). You can read it later with history.state.
  2. unused: an old "title" argument that browsers ignore. Always pass an empty string "".
  3. url: the new address. It must be on the same origin (same protocol, domain and port), so "?tab=reviews", "/about" or "#top" work, but "https://other-site.com" throws an error.

The difference: pushState adds a new entry (Back returns to the previous URL), while replaceState changes the current entry (no new Back step).

MethodAdds history entry?Typical use
pushStateYesUser opens a new tab, page or view
replaceStateNoFixing or updating the current URL, e.g. while typing in a search box

Common mistake: Expecting pushState to show new content. It only changes the URL and history. Updating the page is still your job.

How to handle the Back button with the popstate event

When the user clicks Back or Forward between entries you created, the page does not reload. Instead, the browser fires a popstate event on window, and event.state holds the state object you saved. Your code must listen for it and show the right content.

window.addEventListener("popstate", (event) => {
  console.log("Moved in history. State is:", event.state);
});

history.pushState({ step: 1 }, "", "?step=1");
history.pushState({ step: 2 }, "", "?step=2");
history.back();
// Output: Moved in history. State is: { step: 1 }

Notice that pushState itself does not fire popstate. Only real moves in history (Back, Forward, history.back(), history.go()) do.

Example: tabs that update the URL (a tiny single-page app)

Here is a product page with three tabs. Clicking a tab updates the URL to ?tab=... without reloading, the Back button switches tabs, and opening a shared link shows the right tab straight away.

<!DOCTYPE html>
<html lang="en">
<head>
  <style>
    .active { font-weight: bold; text-decoration: underline; }
  </style>
</head>
<body>
  <nav>
    <a href="?tab=details" data-tab="details">Details</a>
    <a href="?tab=reviews" data-tab="reviews">Reviews</a>
    <a href="?tab=shipping" data-tab="shipping">Shipping</a>
  </nav>
  <section id="panel"></section>

  <script>
    const content = {
      details: "Lightweight running shoe, sizes 36 to 46.",
      reviews: "4.8 stars from 120 customers.",
      shipping: "Free delivery in 2 to 3 days."
    };
    const panel = document.querySelector("#panel");
    const links = document.querySelectorAll("[data-tab]");

    function showTab(tab) {
      if (!content[tab]) tab = "details";          // unknown tab? use default
      panel.textContent = content[tab];
      links.forEach((link) => {
        link.classList.toggle("active", link.dataset.tab === tab);
      });
      document.title = `Shoe - ${tab}`;
    }

    // 1. Clicking a tab: show it and push a new URL
    links.forEach((link) => {
      link.addEventListener("click", (event) => {
        event.preventDefault();                     // stop the full page load
        const tab = link.dataset.tab;
        showTab(tab);
        history.pushState({ tab }, "", `?tab=${tab}`);
      });
    });

    // 2. Back / Forward: show the tab saved in the state
    window.addEventListener("popstate", (event) => {
      showTab(event.state?.tab ?? new URLSearchParams(location.search).get("tab"));
    });

    // 3. First load (or a shared link): read the tab from the URL
    const startTab = new URLSearchParams(location.search).get("tab") ?? "details";
    showTab(startTab);
    history.replaceState({ tab: startTab }, "", location.href);
  </script>
</body>
</html>

Click "Reviews" and then "Shipping": the URL changes each time and the text updates instantly. Now press the browser's Back button: you return to "Reviews", then "Details". The three numbered parts are the pattern every single-page app uses: push on click, listen to popstate, read the URL on load. The replaceState call at the end saves a state for the very first entry, so Back to it also works.

Tip: Keep the real href on your links. If JavaScript fails, or the user opens the link in a new tab with a middle click, the normal URL still works. Your server must also be able to answer those URLs, otherwise a reload shows a "404 Not Found" page.

Practice exercise

Build a three-step signup "wizard" on one page: Step 1 (name), Step 2 (email), Step 3 (done). A "Next" button moves to the next step and calls history.pushState({ step }, "", "?step=" + step). Make the browser's Back button return to the previous step using popstate. When the page loads with ?step=2, start on step 2.

Hint: write one showStep(step) function that hides all steps and shows one. Call it from the button, from popstate (with event.state?.step) and on page load.

Key takeaways

  • history.back(), forward() and go(n) work like the browser's Back and Forward buttons.
  • pushState(state, "", url) adds a history entry and changes the URL without reloading; replaceState changes the current entry.
  • The new URL must be on the same origin, and the middle argument is ignored, so pass "".
  • popstate fires on Back and Forward (not on pushState); use event.state to restore the view.
  • Single-page navigation pattern: push on click, handle popstate, read the URL on first load.