Version v3.1.0 Size 5.5 KB gzip Dependencies Zero dependencies
Why Wayfinder?
Managing navigation by hand means scattered popstate listeners, duplicated path checks, and no shared abstraction for loading data or blocking navigation. Wayfinder moves all of that into one declarative table.
ts
// Before — manual navigation with popstate
window.addEventListener('popstate', () => {
const path = window.location.pathname;
if (path === '/') renderHome();
else if (path.startsWith('/dashboard')) renderDashboard();
else renderNotFound();
});
document.querySelectorAll('a[data-route]').forEach((a) => {
a.addEventListener('click', (e) => {
e.preventDefault();
history.pushState({}, '', (e.currentTarget as HTMLAnchorElement).href);
dispatchEvent(new PopStateEvent('popstate'));
});
});
// After — with Wayfinder
import { createRouter } from '@vielzeug/wayfinder';
const router = createRouter({
routes: {
home: { path: '/' },
dashboard: { path: '/dashboard' },
},
notFound: { data: () => ({ message: 'Page not found' }) },
});
router.subscribe((state) => {
const leaf = state.matches.at(-1);
render(leaf?.data);
});Use Wayfinder when you need named navigation, route-level data loading with cancellation, middleware, or leave guards in a framework-agnostic setup.
Consider a framework's built-in router when you are deep in a single framework ecosystem (React Router, Vue Router) and want first-class component binding with no adapter layer.
| Feature | Wayfinder | page.js | Navigo |
|---|---|---|---|
| Bundle size | 5.5 KB | ~1 kB | ~5 kB |
| History mode | |||
| Memory history (tests / non-browser) | |||
| Typed path params | |||
| Named navigation | Partial | ||
| Middleware | |||
| Data loaders with AbortSignal | |||
| Declarative redirects | |||
| Search param validation | |||
| Error in state | |||
| History state in context | |||
| Leave guards | |||
| Zero dependencies |
Installation
sh
pnpm add @vielzeug/wayfindersh
npm install @vielzeug/wayfindersh
yarn add @vielzeug/wayfinderQuick Start
Create a memory-backed router, wait for initial routing, then navigate by name.
ts
import { createMemoryHistory, createRouter } from '@vielzeug/wayfinder';
const router = createRouter({
history: createMemoryHistory('/'),
routes: {
home: { path: '/' },
settings: { path: '/settings' },
},
});
await router.ready;
await router.navigate({ name: 'settings' });
console.log(router.getSnapshot().location.pathname); // /settings
router.dispose();Features
createRouter()— Compiles named, nested route tables.navigate()— Commits route changes after middleware reaches its terminal stage.ready— Signals that initial routing has settled.data()— Receives cancellation throughAbortSignalfor in-flight loaders.beforeLeave()— Blocks route exits before history changes.match()/load()— Inspect routes synchronously or load detached route data without navigation.preload()— Warms and reuses loader results for the next matching client navigation.createViewRegistry()— Resolves exhaustive typed route views with an explicit not-found fallback.scroll/viewTransition— Coordinate browser effects at the navigation commit boundary.createHashHistory()— Runs browser routes on static hosts that cannot rewrite deep links.createMemoryHistory()— Runs routers in tests and non-browser environments.subscribe()— Reactive subscription to navigation state changes.