Skip to content
wayfinder logoWayfinderRouting
Framework-agnostic client-side router with typed params, async data loading, middleware, leave guards, and cancellation.
Version
v3.1.0
Size
5.5 KB gzip
Dependencies
Zero dependencies
BrowserNode ≥22SSRDeno
createRoutercreateBrowserHistorycreateHashHistorycreateMemoryHistoryredirectTo View all 10 exports

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.

FeatureWayfinderpage.jsNavigo
Bundle size5.5 KB~1 kB~5 kB
History mode
Memory history (tests / non-browser)
Typed path params
Named navigationPartial
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/wayfinder
sh
npm install @vielzeug/wayfinder
sh
yarn add @vielzeug/wayfinder

Quick 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 through AbortSignal for 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.

Documentation

See Also

  • Ripple — reactive signals; sync router state to a signal for framework-agnostic reactivity
  • Ward — permission guards; use inside Wayfinder middleware to protect routes
  • Herald — event bus; dispatch route-change events to decouple navigation side effects