To run code when the URL hash changes in JavaScript, listen for the hashchange event on window. The browser doesn’t reload when only the hash changes — your handler runs each time it does, so you can branch on location.hash without forcing a page refresh.
Last verified: 2026-05-17 in Chrome 124, Firefox 125, Safari 17. Originally published 2022-08-28, rewritten and updated 2026-05-17.
The hashchange event
window.addEventListener('hashchange', () => {
if (location.hash === '#step2') {
// show tutorial step 2
}
});
Every time the hash part of the URL changes — programmatically or via a click on a same-page anchor — the listener fires. No reload, no loss of in-memory state.

Force a reload on hash change (if you really need it)
window.addEventListener('hashchange', () => {
location.reload();
});
Almost never the right call — the whole point of a hash change is that it’s cheap and in-memory. But if your initial render only handles the hash at page load, a reload is the quickest fix.
React to the initial hash on page load too
function handleHash() {
const step = location.hash.slice(1); // 'step2' without the #
if (step === 'step2') {
// ...
}
}
window.addEventListener('hashchange', handleHash);
window.addEventListener('DOMContentLoaded', handleHash);
hashchange doesn’t fire on the very first page load — only on subsequent changes. Run the same handler on DOMContentLoaded (or just inline at the bottom of the script) so the initial hash is also handled.
Read old and new hash from the event
window.addEventListener('hashchange', (event) => {
console.log('Old:', event.oldURL);
console.log('New:', event.newURL);
});
The event object exposes oldURL and newURL — full URLs, not just the hash. Useful for tracking transitions like “user went from #step1 to #step3” for analytics or transition animations.
Frequently asked questions
Hash-only navigations are explicitly designed to not reload — they’re meant for in-page jumps (anchor links) and SPA routing. The browser treats example.com/page#step1 and example.com/page#step2 as the same document, just with a different fragment. The hashchange event exists precisely so you can react to that change without a reload.
window.onhashchange = ... or addEventListener? addEventListener('hashchange', ...) is the modern recommendation. Multiple listeners can coexist on the same event, and there’s no risk of accidentally overwriting another script’s handler — which can happen with window.onhashchange = .... Same browser support.
#? location.hash includes the # (e.g. '#step2'). Strip it with .slice(1) or use the URL constructor: new URL(location.href).hash.slice(1). The History API’s pushState equivalent (history.state) doesn’t include hash semantics.
hashchange if I’m using the History API? If you push real URLs (history.pushState({}, '', '/step2')), use popstate instead. hashchange only fires for the #fragment part. Most SPA routers use pushState/popstate now; hashchange is best for lightweight in-page state without true route changes.
Related guides
- How to Check if a JavaScript String Is a Valid URL
- How to Check the HTTP Referrer with JavaScript
- How to Disable or Enable an Input with JavaScript or jQuery
References
MDN hashchange event: developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event. MDN Location.hash: developer.mozilla.org/en-US/docs/Web/API/Location/hash.