I’m trying to figure out how to build navigation that uses hash fragments in the URL to switch between different sections or views on my page. I want it to work similar to how many web applications handle routing where the URL changes but the page doesn’t actually scroll to an anchor element.
For example, I want URLs like:
mysite.com/app#dashboard
mysite.com/app#settings
mysite.com/app#profile
When someone visits these URLs, I want to show different content or activate different tabs, but I don’t want the browser to scroll down to find an element with that ID.
What’s the best approach for this? Do I need to use JavaScript to watch for hash changes? How can I stop the default scrolling behavior that normally happens with anchor links?
Hit this same problem building a dashboard last year. You need to intercept the hash change before the browser scrolls automatically. event.preventDefault() in a hashchange listener works, but I had better luck handling it on page load too. I used window.addEventListener('hashchange', handleRouting) plus window.addEventListener('load', handleRouting) to catch both nav events and direct URL hits. In the handler, grab the hash with window.location.hash.substring(1) and toggle your content sections. Here’s the catch - don’t give your content containers IDs that match your hash values or the browser will still try scrolling to them. I switched to data attributes for targeting sections instead.
I hit this exact issue building an SPA. You need to stop the default scroll behavior but keep hash routing working. Skip pure hash changes and use history.pushState() and history.replaceState() instead - gives you way more control without triggering scrolls.
On page load, check window.location.hash and route from there. Add window.addEventListener('popstate', handleNavigation) for back/forward buttons. When users click nav items, call history.pushState(null, null, '#dashboard') instead of letting the browser handle it naturally. This completely avoids the scrolling while keeping clean URLs and proper history. The popstate event handles all navigation without fighting browser defaults.
you can also use preventDefault on window scroll when the hash changes. I just do window.addEventListener('hashchange', (e) => { window.scrollTo(0, 0); }) right after catching the route change. It’s a bit hacky but works reliably across browsers without touching the history API. just don’t have actual elements with those hash IDs on your page or things get weird.