npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

better-svelte-router

v1.0.2

Published

A type-safe, reactive router for Svelte 5 applications using runes API

Readme

better-svelte-router

A type-safe, reactive router for Svelte 5 applications using the runes API.

Features

  • 🚀 Svelte 5 Runes - Reactive state management with $state and $derived
  • 🔒 Type Safe - Full TypeScript support with route path autocompletion
  • 🛡️ Navigation Guards - beforeEach and afterEach hooks for route access control
  • 📦 Lazy Loading - Component lazy loading and code splitting support
  • 🔀 Dual Mode Routing - Hash mode and History mode support
  • 📝 Route Meta - Attach custom metadata to routes

Installation

npm install better-svelte-router

Or using other package managers:

# pnpm
pnpm add better-svelte-router

# yarn
yarn add better-svelte-router

Peer Dependencies

This library requires Svelte 5 as a peer dependency:

npm install svelte@^5.0.0

Quick Start

1. Define Route Configuration

// 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' }
          }
        ]
      }
    ]
  }
];

2. Initialize Router Mode

// 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' });

3. Use RouterView Component

<!-- App.svelte -->
<script lang="ts">
  import { RouterView } from 'better-svelte-router';
  import { routes } from './routes';
</script>

<RouterView {routes} />

Router Modes

History Mode (Default)

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' });

Hash Mode

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' });

Programmatic Navigation

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();

Navigation Guards

beforeEach Guard

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 navigation
  • false - Cancel navigation
  • string - Redirect to specified path

afterEach Hook

Executes after navigation completes:

import { afterEach } from 'better-svelte-router';

// Page view tracking
const removeHook = afterEach((from, to) => {
  analytics.trackPageView(to);
});

Reactive State

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>

routerState Properties

| 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 |

Route Meta

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';
  }
});

RouterView Component

Props

| 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 |

Custom Loading and Error States

<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

Nested routes allow you to render child routes within parent components, suitable for layout nesting scenarios.

Route Configuration

// 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 > Security

Parent Layout Component

Parent 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 Component

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 Component

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>

Dynamic Parameters with Nested Routes

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>

API Reference

Navigation Functions

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

Guard Functions

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

Router Mode

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

Route Matching

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[]

Type Definitions

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;
}

Complete Example

Route Configuration

// 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' }
      }
    ]
  }
];

Root Component

<!-- 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} />

Main Layout Component

<!-- 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>

Nested Layout Component

<!-- 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>

Page Component

<!-- 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>

License

MIT