Version v2.0.0 Size 6.1 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: { component: NotFoundPage },
});
router.subscribe((state) => {
render(state.matches.at(-1)?.component);
});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 | 6.1 KB | ~1 kB | ~5 kB |
| History mode | |||
| Memory history (tests / non-browser) | |||
| Typed path params | |||
| Named navigation | Partial | ||
| Middleware | |||
| Data loaders with AbortSignal | |||
| Lazy route loading | |||
| Declarative redirects | |||
| Search param validation | |||
| Error in state | |||
| History state in context | |||
| Leave guards | |||
Hover prefetching (preload()) | |||
| Scroll restoration | |||
| View Transition API | |||
| 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 throughAbortSignaland can stream async-generator updates.beforeLeave()— Blocks route exits before history changes.matchPath()/loadPath()— Inspect routes synchronously or load route data without navigation.preload()— Warms route data for a later matching navigation.createMemoryHistory()— Runs routers in tests and non-browser environments.debugRouter()— Logs navigation state from@vielzeug/wayfinder/devtools.