hashfree
v0.1.0
Published
Ditch the fragment clutter. Hashfree strips # from section links for clean, professional, and high-fidelity URL navigation for single-page websites.
Maintainers
Readme
hashfree
hashfree is a small browser utility for section-based navigation without #hash fragments in the URL.
It watches visible sections with IntersectionObserver, updates the path with the History API, and intercepts in-page anchor clicks so navigation stays clean while still scrolling smoothly.
Features
- No hash fragments in the URL
- Smooth scrolling for anchor links like
#about - Automatic path updates as sections enter view
pushStateorreplaceStateupdate strategies- Small API with TypeScript types included
Installation
# npm
npm install hashfree
# pnpm
pnpm add hashfree
# yarn
yarn add hashfreeHow It Works
Given page sections with id attributes and links that point to those ids, hashfree:
- Prevents the browser from briefly writing
#section-idto the URL. - Smooth-scrolls to the target section when an anchor is clicked.
- Observes visible sections.
- Rewrites the current URL to
/{sectionId}or{basePath}/{sectionId}.
Example:
- Clicking
<a href="#features">Features</a>scrolls to#features - The URL becomes
/featuresinstead of/#features - With
basePath: '/docs', the URL becomes/docs/features
Basic Usage
import { createSectionNav } from 'hashfree';
const nav = createSectionNav({
sections: 'section',
threshold: 0.5,
updateStrategy: 'replace',
basePath: '/docs',
onNavigate: (sectionId) => {
console.log('Visible section:', sectionId);
},
});
// Later
nav.navigateTo('api');
// Cleanup
nav.destroy();HTML Example
<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>import { createSectionNav } from 'hashfree';
createSectionNav({
sections: '[data-section]',
});API
createSectionNav(options?)
Creates section navigation and returns a controller object.
Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| sections | string \| NodeListOf<Element> \| Element[] | '[data-section]' | Selector or explicit list of observed sections |
| rootMargin | string | '0px' | IntersectionObserver root margin |
| threshold | number | 0.5 | Visibility threshold used by the observer |
| updateStrategy | 'push' \| 'replace' | 'replace' | Chooses history.pushState or history.replaceState |
| onNavigate | (sectionId: string) => void | undefined | Called when the most visible observed section changes |
| basePath | string | '' | Prefix added before the section id in the rewritten URL |
| scrollBehavior | 'smooth' \| 'auto' \| 'instant' | auto-detected | Scroll behavior used when scrolling to sections. When omitted, uses 'smooth' unless the user has prefers-reduced-motion: reduce set, in which case 'auto' is used |
Return Value
interface ISectionNavInstance {
destroy: () => void;
navigateTo: (sectionId: string) => void;
}destroy()
Stops observing sections and removes the global click listener.
navigateTo(sectionId)
Smooth-scrolls to a section by its id.
Requirements and Notes
- This library is intended for browser environments.
- Observed sections should have unique
idvalues. - Anchor links should use
href="#section-id". - URL updates use the History API and do not trigger full page reloads.
- Generated paths are normalized to avoid duplicate slashes.
- If your page lives under a nested route such as
/docs, setbasePath: '/docs'so section updates do not rewrite the URL to the site root.
