yourdomain.com/home
no # ever
v0.1.0 - now on npm

URL navigation
without the hash.

A lightweight, framework-agnostic TypeScript library. Scroll through sections and the URL updates cleanly to /about instead of /#about.
Zero dependencies. Under 2kb.

< 2kb gzipped
0 dependencies
ESM + CJS dual build
TypeScript strict mode

Before & after

Standard anchor links append a hash to your URL when users navigate. hashfree intercepts clicks and uses the History API with IntersectionObserver to keep URLs clean at all times.

✕ Without hashfree
yourdomain.com/#about

Hash fragments look unprofessional, clutter shared links, and leak implementation detail to your users.

✓ With hashfree
yourdomain.com/about

Clean, shareable, bookmark-friendly URLs, exactly what your users expect from a modern website.

Up in five lines.

Install from npm, add data-section to your section elements, call createSectionNav() once. Done.

$ pnpm add hashfree
main.ts
import { createSectionNav } from 'hashfree';

const nav = createSectionNav({
  sections: '[data-section]',   // CSS selector or Element[]
  updateStrategy: 'replace',    // 'push' | 'replace'
  threshold: 0.6,               // 0–1, visibility ratio
  onNavigate: (id) => {
    // fires on every section change
    console.log(`navigated to: ${id}`);
  },
});

// later, when the component unmounts:
nav.destroy();

ⓘ Since hashfree rewrites paths (e.g. /about), configure your server to serve the same HTML for all paths, standard SPA fallback.

Every option, documented.

All options are optional. The defaults work for most single-page sites without any configuration.

createSectionNav( options )

Option
Type / Default
Description
sections
string | NodeListOf<Element> | Element[] '[data-section]'
CSS selector, NodeListOf<Element>, or Element array
updateStrategy
'push' | 'replace' 'replace'
Use pushState or replaceState
threshold
number | number[] 0.5
Visibility ratio(s) to trigger (0–1)
rootMargin
string '0px'
IntersectionObserver rootMargin
basePath
string ''
Prepended to every URL write. Set to 'docs' and scrolling to #about writes /docs/about instead of /about
scrollBehavior
ScrollBehavior undefined
'smooth' | 'instant' | 'auto'. Unset by default, auto-resolves to 'auto' when prefers-reduced-motion is active, otherwise 'smooth'
onNavigate
(id: string) => void
Callback fired on each section change

SectionNavInstance

Method
Signature
Description
destroy()
() => void
Disconnects observer, removes all listeners. Call on unmount.
navigateTo(id)
(sectionId: string) => void
Scrolls to a section and focuses it. The URL updates as a side-effect of the IntersectionObserver firing, not synchronously.

Works everywhere.

The vanilla core works in any JavaScript environment. Framework adapters are coming as separate packages so you only ship what you use.

Vanilla JS / TS
Import and call. No framework needed.
✓ ready
🌊
Astro
Works in client-side scripts out of the box.
✓ ready
⚛️
React
useHashfree() hook, coming in hashfree-react.
coming soon
💚
Vue
Composable, coming in hashfree-vue.
coming soon
🔶
Svelte
use:hashfree action, coming in hashfree-svelte.
coming soon
🌐
SSR Safe
Guards on all browser APIs. No crashes on the server.
✓ ready