yoursite.com/#features, yoursite.com/#pricing, yoursite.com/#contact work just fine. But there's a difference between these and yoursite.com/features. The latter is cleaner, more shareable, and just looks more professional.
If you've done a clean up like this before, you know it requires wiring up IntersectionObserver to track visible sections, intercepting anchor clicks, and using the History API to rewrite the URL. After writing this logic a couple of times across various projects, I packaged it into a lightweight browser utility called hashfree and here's how it works:
Installation
# npm
npm install hashfree
# pnpm
pnpm add hashfree
# yarn
yarn add hashfree
Basic Setup
import { createSectionNav } from 'hashfree';
createSectionNav({
sections: '[data-section]',
});
Your HTML stays exactly as you'd expect
Keep your anchor links as they are, hashfree intercepts them without requiring any changes to your markup:
<nav>
<a href="#intro">Intro</a>
<a href="#features">Features</a>
<a href="#api">API</a>
</nav>
<section id="intro" data-section>...</section>
<section id="features" data-section>...</section>
<section id="api" data-section>...</section>
Clicking #features smooth scrolls to the section and rewrites the URL to /features, no hash, no full page reload.
More Configuration
hashfree ships with a few more options: basePath for nested routes, updateStrategy to control browser history, onNavigate callbacks, programmatic navigation via navigateTo(), and more.
Full configuration reference on npm and the docs.π
A note on routers
hashfree is intentionally not a router. If you're building a full SPA with dynamic routes, your framework's routing solution is the right call. But if you have a landing page, docs site, or portfolio with section-based navigation and just want clean URLs, this is the one.
Top comments (0)