W3docs

JavaScript History API

In modern web development, creating seamless user experiences often involves manipulating the browser's history. The JavaScript History API provides the tools

Introduction to the JavaScript History API

In modern web development, creating seamless user experiences often involves manipulating the browser's history. The JavaScript History API lets you read and change the browser's session history — the list of pages (and URL states) the user has visited in the current tab. By using this API, developers can update the address bar and the back/forward stack without triggering a full page reload, which is exactly what single-page applications (SPAs) need to feel fast and native.

This page covers the three core methods (pushState, replaceState, and the navigation helpers), how to react to the popstate event, the common gotchas that trip people up, and the best practices for using the API in production.

Why the History API exists

Before this API, the only way to change the URL was to navigate, which reloaded the whole document. SPAs render new "pages" with JavaScript, so they need the URL to stay in sync without a reload — otherwise the back button, bookmarks, and shareable links all break.

The History API solves this by giving you a way to:

  • Add a new entry to the back/forward stack (pushState).
  • Modify the current entry in place (replaceState).
  • React when the user moves through that stack with the back/forward buttons (popstate).

You read the current entry's data through the read-only history.state property, and history.length tells you how many entries are in the session stack. To build the URLs you pass to these methods, the URL object is a useful companion.

The history object at a glance

The API is exposed on the global window.history object (you can write just history):

MemberWhat it does
history.pushState(state, title, url)Adds a new entry to the history stack and updates the URL.
history.replaceState(state, title, url)Replaces the current entry — no new stack entry is created.
history.stateRead-only copy of the state object of the current entry.
history.lengthNumber of entries in the session history.
history.back()Goes back one entry (same as the browser back button).
history.forward()Goes forward one entry.
history.go(n)Jumps n entries (negative = back, positive = forward).

Utilizing the History API in Web Applications

The History API allows for navigation between different states of an application without reloading the page. pushState() takes three arguments — a state object (any serializable value), a title (ignored by most browsers, so pass an empty string), and a url (resolved relative to the current page and must be same-origin). Here is how to push a new state:

<div>
    <button onclick="changeState()">Go to New State</button>
</div>

<script>
    // Function to change state
    function changeState() {
        const newState = { id: 'newState' };
        // Push a new state to the history stack
        window.history.pushState(newState, 'New State', 'new-state-url');
    }
</script>

This adds a new state to the history stack using pushState(). Notice what it does not do: it does not load new-state-url, and it does not fire a popstate event. pushState only updates the address bar and the stack — rendering the matching content is your job.

Handling the Popstate Event

When the user clicks the browser's back or forward button (or you call history.back() / history.forward()), the popstate event fires. The event's state property holds the state object you previously passed to pushState/replaceState for the entry now being activated. Handle it to restore the right view:

window.addEventListener('popstate', function(event) {
        if(event.state) {
            console.log('State changed:', event.state);
            // Handle the state object here
        }
    });

This listener reacts to popstate events, logging changes and allowing for state adjustments based on the user's navigation history. When event.state is null, the user has navigated back to an entry created by a normal page load (one that never received a state object), so render your default view.

Replacing the Current State

Sometimes you want to update the current entry without adding a new record to the stack — for example, syncing a filter or a tab selection into the URL where adding a back-button step would be annoying. That is what replaceState() is for:

<div>
    <button onclick="replaceCurrentState()">Replace State</button>
    <p id="replace-status">Ready</p>
</div>

<script>
    function replaceCurrentState() {
        const newState = { id: 'replacedState' };
        // Replace the current state
        window.history.replaceState(newState, 'Replaced State', 'replaced-state-url');
        document.getElementById('replace-status').textContent = 'State replaced successfully!';
    }
</script>

This updates the current entry in place. The back button still goes to whatever page came before, because replaceState did not push a new step.

Full SPA-style example

Now let's bring everything together. The example below simulates a single-page application: clicking a button swaps the content of a div and updates the URL with pushState, while a popstate listener restores the correct content when the user navigates with the back/forward buttons. This is the same pattern a client-side router uses under the hood.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>SPA Style History API Example</title>
</head>
<body>
    <h1>Page Navigation with History API</h1>
    <div id="content">Start Page</div>

    <!-- Buttons for navigation -->
    <button onclick="loadPage('page1')">Load Page 1</button>
    <button onclick="loadPage('page2')">Load Page 2</button>
    <button onclick="manualGoBack()">Go Back</button>
    <button onclick="manualGoForward()">Go Forward</button>

    <!-- Display the current status of the history -->
    <p id="historyStatus">History Status: Start</p>

    <script>
        // Loads a "page" and updates the browser's history state
        function loadPage(page) {
            const state = { page: page }; // State to be pushed to history
            history.pushState(state, `Page ${page}`, `${page}.html`); // Pushing state to the history
            document.getElementById('content').innerHTML = `<h2>This is ${page.replace('page', 'Page ')}</h2>`; // Update the content
            updateHistoryStatus(state); // Update the history status display
        }

        // Handles the browser's back and forward button actions
        window.addEventListener('popstate', function(event) {
            if (event.state) {
                // Update the page content and history status when navigating through history
                document.getElementById('content').innerHTML = `<h2>This is ${event.state.page.replace('page', 'Page ')}</h2>`;
                updateHistoryStatus(event.state);
            } else {
                // Fallback content when the history does not have any state
                document.getElementById('content').innerHTML = `<h2>Start Page</h2>`;
                document.getElementById('historyStatus').textContent = "History Status: Start";
            }
        });

        // Updates the display of the current history status
        function updateHistoryStatus(state) {
            document.getElementById('historyStatus').textContent = `History Status: ${state.page}`;
        }

        // Function to manually trigger going back in history
        function manualGoBack() {
            history.back();
        }

        // Function to manually trigger going forward in history
        function manualGoForward() {
            history.forward();
        }
    </script>
</body>
</html>

How it works:

  • Dynamic page loadingloadPage() changes the content of a div and calls pushState to add a history entry, so each "page" becomes a real back/forward stop.
  • Restoring on navigation — the popstate listener reads event.state.page and re-renders the matching content; when event.state is null it falls back to the start page.
  • Familiar UI — the buttons drive history.back() and history.forward(), giving a multi-page feel without a single full reload.

Common gotchas

A few behaviors regularly surprise developers:

  • pushState does not fire popstate. Only user navigation (back/forward, history.go()) fires it. After a pushState, render the new view yourself in the same function.
  • The title argument is ignored. Almost every browser disregards it, so pass "". To change the tab title, set document.title directly.
  • URLs must be same-origin. Passing a cross-origin url throws a SecurityError. The path is resolved relative to the current document.
  • State must be serializable. The state object is cloned with the structured clone algorithm, so functions, DOM nodes, and class instances can't be stored — and there is a size limit (commonly a few MB).
  • A page reload re-runs your app at the new URL. When the user refreshes a pushState-d URL, the server must respond to that path (or your SPA must handle it on load), otherwise they get a 404. This server-side fallback is the routing concern SPAs must solve.

Best Practices for Using the History API

  • Keep state small. Store an identifier or a few primitives in the state object and re-derive the rest. Don't dump large datasets into history — it bloats the session and risks hitting the size limit.
  • Always update document.title yourself when the "page" changes, since the title argument is ignored.
  • Manage scroll position. Browsers restore scroll on popstate via history.scrollRestoration; set it to "manual" if you want to control scrolling yourself. See window sizes and scrolling for the scroll APIs.
  • Provide a server-side fallback so refreshing or sharing a pushState-d URL still works.
  • Use replaceState for non-navigational updates (filters, tabs) so the back button stays meaningful.
  • Inspecting history — use history.state for the current state object and history.length for the number of entries in the session stack.

Because the History API only changes the URL, it pairs naturally with fetch for loading the data each "page" needs.

Conclusion

The JavaScript History API lets you manipulate the browser's session history — pushing, replacing, and reacting to navigation — without full page reloads. Mastering pushState, replaceState, and the popstate event, while respecting the gotchas around popstate, serialization, and same-origin URLs, is what makes single-page applications feel as fast and navigable as traditional multi-page sites.

Practice

Practice
Which of the following are functionalities of the JavaScript History API?
Which of the following are functionalities of the JavaScript History API?
Was this page helpful?