A type-safe, reactive router for Svelte 5 applications using the runes API.
$state and $derivedbeforeEach and afterEach hooks for route access controlnpm install better-svelte-router
Or using other package managers:
# pnpm
pnpm add better-svelte-router
# yarn
yarn add better-svelte-router
This library requires Svelte 5 as a peer dependency:
npm install svelte@^5.0.0
// routes.ts
import type { IRoute } from 'better-svelte-router';
export const routes: IRoute[] = [
{
path: '/',
redirect: '/home',
meta: { title: 'Home' },
children: [
{
path: 'home',
component: () => import('./pages/Home.svelte'),
meta: { title: 'Home Page' }
},
{
path: 'users',
component: () => import('./pages/Users.svelte'),
meta: { title: 'Users', requiresAuth: true },
children: [
{
path: ':id',
component: () => import('./pages/UserDetail.svelte'),
meta: { title: 'User Detail' }
}
]
}
]
}
];
// main.ts or App.svelte
import { createRouterMode } from 'better-svelte-router';
// History mode (recommended)
createRouterMode({ mode: 'history' });
// Or Hash mode
createRouterMode({ mode: 'hash' });
// With base path
createRouterMode({ mode: 'history', base: '/my-app' });
<!-- App.svelte -->
<script lang="ts">
import { RouterView } from 'better-svelte-router';
import { routes } from './routes';
</script>
<RouterView {routes} />
Uses HTML5 History API with URL format /path:
import { createRouterMode } from 'better-svelte-router';
// Initialize history mode
createRouterMode({ mode: 'history' });
// With base path (for apps deployed in subdirectories)
createRouterMode({ mode: 'history', base: '/app' });
Uses URL hash with format /#/path, suitable for environments without server-side configuration:
import { createRouterMode } from 'better-svelte-router';
// Initialize hash mode
createRouterMode({ mode: 'hash' });
import { push, replace, back, forward } from 'better-svelte-router';
// Navigate to a new route (adds history entry)
await push('/users');
// With query parameters
await push('/search', { q: 'test', page: 1 });
// Replace current history entry
await replace('/login');
// Browser history navigation
back();
forward();
Executes before navigation occurs, can cancel or redirect navigation:
import { beforeEach } from 'better-svelte-router';
// Authentication guard
const removeGuard = beforeEach((from, to) => {
if (to.startsWith('/admin') && !isAuthenticated) {
return '/login'; // Redirect to login page
}
return true; // Allow navigation
});
// Remove guard
removeGuard();
Guard return values:
true or void - Allow navigationfalse - Cancel navigationstring - Redirect to specified pathExecutes after navigation completes:
import { afterEach } from 'better-svelte-router';
// Page view tracking
const removeHook = afterEach((from, to) => {
analytics.trackPageView(to);
});
Access current route state using routerState:
<script lang="ts">
import { routerState } from 'better-svelte-router';
// Reactive access to route state
$effect(() => {
console.log('Current path:', routerState.pathname);
console.log('Query params:', routerState.query);
console.log('Route meta:', routerState.meta);
// Update page title
document.title = routerState.meta.title ?? 'App';
});
</script>
<p>Current path: {routerState.pathname}</p>
<p>Query params: {JSON.stringify(routerState.query)}</p>
| Property | Type | Description |
|---|---|---|
href |
string |
Full URL |
pathname |
string |
Current path |
search |
string |
Query string (including ?) |
hash |
string |
URL hash (including #) |
query |
Record<string, string> |
Parsed query parameters |
meta |
RouteMeta |
Current route metadata |
Attach custom metadata to routes:
import type { IRoute } from 'better-svelte-router';
const routes: IRoute[] = [
{
path: '/admin',
component: AdminLayout,
meta: {
title: 'Admin Panel',
requiresAuth: true,
permissions: ['admin']
}
}
];
Use meta in guards:
import { beforeEach, matchRoute } from 'better-svelte-router';
import { routes } from './routes';
beforeEach((from, to) => {
const matched = matchRoute(routes, to);
if (matched?.meta.requiresAuth && !isAuthenticated) {
return '/login';
}
});
| Prop | Type | Description |
|---|---|---|
routes |
IRoute[] |
Route configuration array |
prefix |
string |
Path prefix (for nested routes) |
error |
Snippet<[Error]> |
Custom error display |
loading |
Snippet |
Custom loading display |
<script lang="ts">
import { RouterView } from 'better-svelte-router';
import { routes } from './routes';
</script>
{#snippet loading()}
<div class="loading">Loading...</div>
{/snippet}
{#snippet error(err)}
<div class="error">
<h2>Failed to load</h2>
<p>{err.message}</p>
</div>
{/snippet}
<RouterView {routes} {loading} {error} />
Nested routes allow you to render child routes within parent components, suitable for layout nesting scenarios.
// routes.ts
import type { IRoute } from 'better-svelte-router';
export const routes: IRoute[] = [
{
path: '/',
component: () => import('./layouts/MainLayout.svelte'),
children: [
{
path: 'dashboard',
component: () => import('./pages/Dashboard.svelte'),
meta: { title: 'Dashboard' }
},
{
path: 'settings',
component: () => import('./layouts/SettingsLayout.svelte'),
meta: { title: 'Settings' },
children: [
{
path: 'profile',
component: () => import('./pages/settings/Profile.svelte'),
meta: { title: 'Profile Settings' }
},
{
path: 'security',
component: () => import('./pages/settings/Security.svelte'),
meta: { title: 'Security Settings' }
}
]
}
]
}
];
The above configuration generates these routes:
/dashboard → MainLayout > Dashboard/settings/profile → MainLayout > SettingsLayout > Profile/settings/security → MainLayout > SettingsLayout > SecurityParent components need to use RouterView to render child routes, passing the current path prefix via the prefix prop:
<!-- layouts/MainLayout.svelte -->
<script lang="ts">
import { RouterView } from 'better-svelte-router';
import type { IRoute } from 'better-svelte-router';
interface Props {
routes: IRoute[];
prefix?: string;
}
let { routes, prefix = '' }: Props = $props();
</script>
<div class="main-layout">
<header>
<nav>
<a href="/dashboard">Dashboard</a>
<a href="/settings/profile">Settings</a>
</nav>
</header>
<main>
<!-- Render child routes -->
<RouterView {routes} {prefix} />
</main>
<footer>© 2024 My App</footer>
</div>
Nested layout components also receive routes and prefix, continuing to pass them down:
<!-- layouts/SettingsLayout.svelte -->
<script lang="ts">
import { RouterView } from 'better-svelte-router';
import type { IRoute } from 'better-svelte-router';
interface Props {
routes: IRoute[];
prefix?: string;
}
let { routes, prefix = '' }: Props = $props();
</script>
<div class="settings-layout">
<aside>
<nav>
<a href="/settings/profile">Profile</a>
<a href="/settings/security">Security</a>
</nav>
</aside>
<section class="settings-content">
<!-- Render settings sub-pages -->
<RouterView {routes} {prefix} />
</section>
</div>
Leaf page components don't need to render child routes and can display content directly:
<!-- pages/settings/Profile.svelte -->
<script lang="ts">
import { routerState } from 'better-svelte-router';
</script>
<div class="profile-page">
<h1>Profile Settings</h1>
<p>Current path: {routerState.pathname}</p>
<!-- Page content -->
</div>
Nested routes support dynamic parameters, which are automatically passed to matched components:
const routes: IRoute[] = [
{
path: '/',
component: () => import('./layouts/MainLayout.svelte'),
children: [
{
path: 'users/:userId',
component: () => import('./layouts/UserLayout.svelte'),
children: [
{
path: 'posts',
component: () => import('./pages/UserPosts.svelte')
},
{
path: 'posts/:postId',
component: () => import('./pages/PostDetail.svelte')
}
]
}
]
}
];
Access route parameters in components:
<!-- pages/PostDetail.svelte -->
<script lang="ts">
interface Props {
params: { userId: string; postId: string };
}
let { params }: Props = $props();
</script>
<div>
<h1>Post {params.postId}</h1>
<p>By User {params.userId}</p>
</div>
import { push, replace, back, forward, buildSearchString } from 'better-svelte-router';
// Navigate to a new route
push(to: RoutePath, query?: QueryParams): Promise<boolean>
// Replace current route
replace(to: RoutePath, query?: QueryParams): Promise<boolean>
// Go back
back(): void
// Go forward
forward(): void
// Build query string
buildSearchString(query?: QueryParams): string
import { beforeEach, afterEach, clearGuards } from 'better-svelte-router';
// Register before guard
beforeEach(guard: NavigationGuard): () => void
// Register after hook
afterEach(hook: AfterEachHook): () => void
// Clear all guards (for testing)
clearGuards(): void
import { createRouterMode, getRouterMode, resetRouterMode } from 'better-svelte-router';
// Create router mode
createRouterMode(config: RouterModeConfig): IRouterModeAdapter
// Get current router mode adapter
getRouterMode(): IRouterModeAdapter
// Reset router mode (for testing)
resetRouterMode(): void
import { matchRoute, findMatchingRoutes } from 'better-svelte-router';
// Match a single route
matchRoute(routes: IRoute[], pathname: string, prefix?: string): MatchedRoute | null
// Find all matching routes (including parents)
findMatchingRoutes(routes: IRoute[], pathname: string, prefix?: string): MatchedRoute[]
import type {
IRoute,
RouteMeta,
QueryParams,
NavigationGuard,
RouterMode,
RouterModeConfig,
MatchedRoute
} from 'better-svelte-router';
// Route configuration
interface IRoute {
path: string;
name?: string;
component?: Component | LazyComponent;
children?: IRoute[];
redirect?: string;
meta?: RouteMeta;
}
// Route metadata
interface RouteMeta {
title?: string;
requiresAuth?: boolean;
[key: string]: unknown;
}
// Query parameters
type QueryParams = Record<string, string | number | boolean | undefined | null>;
// Navigation guard
type NavigationGuard = (from: string, to: string) =>
boolean | string | void | Promise<boolean | string | void>;
// Router mode
type RouterMode = 'hash' | 'history';
interface RouterModeConfig {
mode: RouterMode;
base?: string;
}
// routes.ts
import type { IRoute } from 'better-svelte-router';
export const routes: IRoute[] = [
{
path: '/',
redirect: '/dashboard'
},
{
path: '/',
component: () => import('./layouts/MainLayout.svelte'),
children: [
{
path: 'dashboard',
component: () => import('./pages/Dashboard.svelte'),
meta: { title: 'Dashboard' }
},
{
path: 'users',
component: () => import('./layouts/UsersLayout.svelte'),
meta: { title: 'Users', requiresAuth: true },
children: [
{
path: '',
component: () => import('./pages/users/UserList.svelte'),
meta: { title: 'User List' }
},
{
path: ':id',
component: () => import('./pages/users/UserDetail.svelte'),
meta: { title: 'User Detail' }
}
]
},
{
path: 'login',
component: () => import('./pages/Login.svelte'),
meta: { title: 'Login' }
}
]
}
];
<!-- App.svelte -->
<script lang="ts">
import {
RouterView,
routerState,
beforeEach,
afterEach,
createRouterMode
} from 'better-svelte-router';
import { routes } from './routes';
// Initialize router mode
createRouterMode({ mode: 'history' });
// Authentication guard
beforeEach((from, to) => {
if (to.startsWith('/users') && !localStorage.getItem('token')) {
return '/login';
}
return true;
});
// Page view tracking
afterEach((from, to) => {
console.log(`Navigated: ${from} -> ${to}`);
});
// Update page title
$effect(() => {
document.title = routerState.meta.title ?? 'My App';
});
</script>
{#snippet loading()}
<div class="flex items-center justify-center h-screen">
<span class="loading loading-spinner loading-lg"></span>
</div>
{/snippet}
{#snippet error(err)}
<div class="alert alert-error">
<span>Failed to load page: {err.message}</span>
</div>
{/snippet}
<RouterView {routes} {loading} {error} />
<!-- layouts/MainLayout.svelte -->
<script lang="ts">
import { RouterView } from 'better-svelte-router';
import type { IRoute } from 'better-svelte-router';
interface Props {
routes: IRoute[];
prefix?: string;
}
let { routes, prefix = '' }: Props = $props();
</script>
<div class="app-container">
<header>
<nav>
<a href="/dashboard">Dashboard</a>
<a href="/users">Users</a>
</nav>
</header>
<main>
<RouterView {routes} {prefix} />
</main>
</div>
<!-- layouts/UsersLayout.svelte -->
<script lang="ts">
import { RouterView } from 'better-svelte-router';
import type { IRoute } from 'better-svelte-router';
interface Props {
routes: IRoute[];
prefix?: string;
}
let { routes, prefix = '' }: Props = $props();
</script>
<div class="users-layout">
<aside>
<h3>Users Menu</h3>
<a href="/users">All Users</a>
</aside>
<section>
<RouterView {routes} {prefix} />
</section>
</div>
<!-- pages/users/UserDetail.svelte -->
<script lang="ts">
interface Props {
params: { id: string };
}
let { params }: Props = $props();
</script>
<div>
<h1>User Detail</h1>
<p>User ID: {params.id}</p>
</div>
MIT