JavaScriptBrowser APIs

history.pushState()

Adds a new entry to the browser history and changes the address bar without reloading the page.

Returns
undefined
On this page

Example

#
JavaScript
function showLesson(number) {
  document.querySelector('#title').textContent = `Lesson ${number}`
}

function goToLesson(number) {
  showLesson(number)
  history.pushState({ lesson: number }, '', `/courses/javascript/lesson-${number}`)
}

goToLesson(6)

console.log(location.pathname)
console.log(history.state)
Result
/courses/javascript/lesson-6
{ lesson: 6 }

In the isolated preview frame, changing the address is blocked (SecurityError), so this example doesn’t run here. On a regular page, the address bar changes to /courses/javascript/lesson-6, and the page doesn’t reload.

Definition and usage

#

The history.pushState() method changes the address shown in the browser’s address bar and adds a new entry to the tab’s history, but doesn’t reload the page or fetch anything from the server. Single-page applications (SPAs) rely on this: you change the content with JavaScript, and the address keeps up with what the user sees, so it can be copied, bookmarked, or refreshed.

The first argument is the data (state) tied to the entry, e.g. { lesson: 5 }. It must be cloneable, the same way structuredClone() works, so functions and DOM elements are out. Browsers ignore the second argument, but you still need to pass it, usually as an empty string. The third is the new address: it can be relative, but must belong to the same origin, or the method throws.

When the user clicks “Back” or “Forward”, the browser fires a popstate event on window and passes the data saved with the entry in event.state. Your job then is to show the matching content. pushState() itself does not trigger a popstate event.

The sibling method history.replaceState() takes the same arguments but replaces the current entry instead of adding a new one. It’s handy for things like filter changes that shouldn’t fill up the history. Keep in mind that after a page refresh the browser asks the server for the new address, so the server has to be able to handle it.

Syntax

#
Syntax
history.pushState(state, unused)
history.pushState(state, unused, url)

history.replaceState(state, unused, url)
history.state
window.addEventListener("popstate", listener)

Parameters

#
  • state

    any cloneable value

    The data saved with the history entry. It comes back in event.state of the popstate event, and in history.state.
  • unused

    string

    A parameter kept for historical reasons. Pass an empty string "".
  • url

    optional, string or URL

    The new address, relative or absolute, from the same origin as the page. Without it, the address doesn’t change.

More examples

#
Handling the Back button with popstate
JavaScript
const title = document.querySelector('#title')

function render(lesson) {
  title.textContent = `Lesson ${lesson}`
  console.log(`Showing lesson ${lesson}`)
}

window.addEventListener('popstate', (event) => {
  render(event.state?.lesson ?? 1)
})

history.pushState({ lesson: 6 }, '', '/courses/javascript/lesson-6')
render(6)
history.pushState({ lesson: 7 }, '', '/courses/javascript/lesson-7')
render(7)

history.back()
Result
Showing lesson 6
Showing lesson 7
Showing lesson 6

history.back() acts like clicking “Back”: the browser goes back to the previous entry and fires a popstate event with the saved state { lesson: 6 }. The page doesn’t reload, and the render() function redraws the content.

replaceState() on a filter change
JavaScript
function applyFilter(level) {
  const url = new URL(location.href)
  url.searchParams.set('level', level)
  history.replaceState(null, '', url)
  console.log(location.search)
}

applyFilter('beginner')
applyFilter('intermediate')
Result
?level=beginner
?level=intermediate

Each call replaces the current history entry, so clicking “Back” doesn’t step back through the filters one by one; it goes straight to the previous page. The result assumes a page that started out with no parameters in its address.

Browser support

#

Widely available

Works in all modern browsers, phones included. You can use it without worry.

  • Chrome
  • Edge
  • Firefox
  • Safari

Good practices

#
  • After pushState(), always handle the popstate event. Without it, the “Back” button changes the address, but the page content stays the old one.
  • Save only simple data in the state, such as a lesson id. Functions and DOM elements can’t be cloned and will cause a DataCloneError.
  • Make sure the server handles the addresses created by pushState(). After a page refresh or opening a bookmark, the browser will ask the server for them.

Related entries

#

Spotted a mistake or missing an example? Write to us.