URL Hash Fragment Routing Guide
Everything after the # in a URL is the hash fragment — and it occupies a unique position in web architecture. Unlike the path and query string, the fragment is never sent to the server. It exists entirely within the browser, which makes it the original mechanism for client-side routing in single-page applications (SPAs). Understanding how fragment routing works helps you debug navigation issues, implement anchor links, and choose between hash-based and history-based routing.
How Hash Fragments Work
When a browser loads https://example.com/page#section-2, it:
- Sends a request for
https://example.com/page(the fragment#section-2is omitted) - Renders the page
- Scrolls to the element with
id="section-2"
If JavaScript later changes the fragment via location.hash = 'section-3', the browser updates the URL and scrolls to the new anchor — but it does not make a new server request.
The HashChangeEvent
Browsers fire a hashchange event whenever the fragment changes, whether from user navigation or JavaScript:
window.addEventListener('hashchange', (event) => {
console.log('Old:', event.oldURL);
console.log('New:', event.newURL);
});
This event is the foundation of hash-based routing: your JavaScript listens for hashchange and re-renders the appropriate view.
Hash-Based Routing in SPAs
Before the History API, every SPA used hash fragments for routing. The pattern looks like this:
https://example.com/#/dashboard
https://example.com/#/users/42
https://example.com/#/settings?tab=security
The #/ prefix signals that the fragment contains a route path rather than an anchor ID. Here is a minimal router:
const routes = {
'/dashboard': renderDashboard,
'/users/:id': renderUserProfile,
'/settings': renderSettings,
};
function navigate() {
const path = location.hash.slice(1); // Remove #
const handler = matchRoute(routes, path);
if (handler) handler();
}
window.addEventListener('hashchange', navigate);
navigate(); // Initial render
Advantages of Hash Routing
| Advantage | Why |
|---|---|
| No server configuration | The server only sees the base URL — no need for catch-all rewrites |
| Works on static hosting | Fragment routes never generate 404s because the server always serves the same HTML |
| Broad browser support | hashchange works in every browser including IE8 |
| Preserves back/forward | Browser history tracks fragment changes automatically |
Disadvantages of Hash Routing
| Disadvantage | Why |
|---|---|
| Ugly URLs | /#/users/42 is less clean than /users/42 |
| Fragments are not sent to server | Server-side analytics and logging lose route information |
| SEO limitations | Search engines historically ignored fragments (now partially indexed but still suboptimal) |
| Anchor conflict | You cannot distinguish between #section-id (anchor) and #/route (page) without a convention |
History-Based Routing (The Modern Alternative)
The History API (pushState, replaceState, popstate) enables routing without hash fragments:
history.pushState(null, '', '/users/42');
window.addEventListener('popstate', () => {
renderRoute(location.pathname);
});
History routing produces clean URLs (/users/42), but it requires server-side configuration to return the SPA's HTML for all routes — otherwise a direct visit to /users/42 returns a 404.
| Feature | Hash Routing | History Routing |
|---|---|---|
| Server config needed | No | Yes (catch-all) |
| Clean URLs | No (/#/...) | Yes (/...) |
| SEO-friendly | Limited | Full |
| Fragment tracking | Not sent to server | Full path visible to server |
| Browser support | IE8+ | IE10+ |
When to Use Hash Routing
Hash routing remains the right choice in several scenarios:
- Static hosting without rewrites — GitHub Pages, Netlify static drops, or S3 buckets where you cannot configure fallback routing
- Embedded widgets — iframes or micro-frontends where you cannot control the host page's server
- Email and SMS deep links — links where the server must always return a valid page, and the fragment determines client-side state
- Progressive enhancement — applications that must work even if JavaScript fails to load (the server-rendered page always loads; the fragment adds SPA navigation)
Combining Fragment Routing with Anchor Links
If your app uses hash routing (#/route) but also needs traditional anchor links, you need a convention to disambiguate. A common pattern:
#/docs/installation#prerequisites
Some routers (like Vue Router's hash mode) support this by splitting the fragment at the second #. Others require you to use query parameters for anchors:
#/docs/installation?anchor=prerequisites
Key Takeaways
- Hash fragments are never sent to the server, making them ideal for purely client-side routing.
hashchangeevents power hash-based routers without requiring server configuration.- History-based routing produces cleaner, more SEO-friendly URLs but requires server-side catch-all configuration.
- Use hash routing for static hosting, embedded widgets, and scenarios where server configuration is out of your control.
- When mixing hash routing with anchor links, adopt a consistent convention to distinguish routes from scroll targets.
Try It Yourself
Building or debugging a routed application? The URL Parser breaks any URL into its components — scheme, host, path, query parameters, and fragment — so you can verify that your routes are structured correctly.