svelte5-spa-router Svelte Themes

Svelte5 Spa Router

๐Ÿ†• v1.1.8 & v1.1.9: Reactive locationStore for Layout Control

New: locationStore for Layout Reactivity

You can now use a reactive Svelte store to track the current location (pathname, search, hash) for advanced layout logic (e.g. sidebar, header, breadcrumbs) in your app:

import { locationStore } from 'svelte5-spa-router';
$: $locationStore.pathname; // Reacts to path changes

locationStore is always up-to-date with browser navigation, pushState, replaceState, and popstate events.

Example: Hide Sidebar on Login Page

<script>
    import { locationStore } from 'svelte5-spa-router';
    $: hideSidebar = $locationStore.pathname === '/login';
</script>

{#if !hideSidebar}
    <Sidebar />
{/if}

Changelog (Recent)

v1.2.2 Changelog & Migration

๐Ÿš€ What's New in v1.2.2

  • Multi-level & Nested Route Support:
    • Flat route definitions for deeply nested paths (e.g. /multi/:parentId/child/:childId/grandchild/:grandId).
  • Improved Route Guards:
    • Role-based, async, and parameterized guards with beforeEnter.
  • Cypress E2E Testing:
    • Full coverage for all routes, including nested and guarded endpoints.
  • Demo & Docs Update:
    • README now includes explicit examples for nested/multi routes and guard usage.
  • Bug Fixes & Stability:
    • Improved param parsing, fallback handling, and route matching.

โšก Migration Notes

  • Flat Route Definitions:
    • Replace nested/children arrays with flat path strings for all routes.
    • Example:
      // Old (nested)
      { path: '/multi/:parentId', children: [ ... ] }
      // New (flat)
      { path: '/multi/:parentId/child/:childId', component: MultiChild }
      
  • Route Guards:
    • Use beforeEnter on each route for sync/async/role checks.
  • Testing:
    • Use Cypress for browser-based E2E tests (cypress/e2e/routes.integration.cy.js).
  • No Breaking Changes:
    • Existing array-based config and imperative router.addRoute still supported.

See the updated README and demo for usage patterns and migration examples.


Svelte 5 SPA Router โ€“ Universal Routing Example

๐Ÿš€ Quick Start

Important for Universal SPA: To ensure routing works on all paths (e.g. /login, /about), make sure your static server rewrites all requests to index.html (see Troubleshooting below).

1. Install

npm install svelte5-spa-router

2. Definisikan Route dan Komponen (REKOMENDASI UTAMA)

<script>
    import Router from 'svelte5-spa-router/Router.svelte';
    import Link from 'svelte5-spa-router/Link.svelte';
    import { goto, routeParams, queryParams } from 'svelte5-spa-router';

    import Home from './Home.svelte';
    import About from './About.svelte';
    import Blog from './Blog.svelte';
    import BlogPost from './BlogPost.svelte';
    import UserProfile from './UserProfile.svelte';
    import Search from './Search.svelte';
    import NotFound from './NotFound.svelte';

    // Array-based config (recommended for all universal SPAs)
    const routes = [
        { path: '/', component: Home },
        { path: '/about', component: About },
        { path: '/blog', component: Blog },
        { path: '/blog/:id', component: BlogPost },
        { path: '/user/:id', component: UserProfile },
        { path: '/search/:query?', component: Search }
    ];

    function navigateToBlog() {
        goto('/blog/my-first-post');
    }
    function navigateWithQuery() {
        goto('/search', { q: 'svelte', category: 'frontend' });
    }
    function searchBlog() {
        goto('/blog', { search: 'router' });
    }
</script>

<nav>
    <Link href="/">Home</Link>
    <Link href="/about">About</Link>
    <Link href="/blog">Blog</Link>
    <Link href="/user/123">User Profile</Link>
    <Link href="/search">Search</Link>
    <button on:click={navigateToBlog}>Go to Blog Post</button>
    <button on:click={navigateWithQuery}>Search with Query</button>
    <button on:click={searchBlog}>Search Blog</button>
</nav>

<Router {routes} fallback={NotFound} />

<!-- Access params in your component -->
<p>Route Params: {JSON.stringify($routeParams)}</p>
<p>Query Params: {JSON.stringify($queryParams)}</p>

Note:

  • The array-based config above is the default and most recommended for universal SPAs.
  • The imperative way (router.addRoute) is only for advanced use-cases (e.g. dynamic/plugin route injection), not needed for most apps.

  • Always import from the package (svelte5-spa-router), not from src/lib/.
  • To access params, use $routeParams and $queryParams in your template.
  • No SvelteKit required, works directly with Vite + Svelte 5.

๐Ÿš€ Svelte 5 SPA Router on SvelteKit runes support

A simple, flexible, and lightweight SPA router specifically designed for Svelte 5 with runes support.

โœจ Features

  • ๐ŸŽฏ Svelte 5 Native: Built from ground up for Svelte 5 with runes
  • ๐Ÿ›ฃ๏ธ Dynamic Routing: Support for parameters (:id), optional parameters (:id?), and wildcards (/*)
  • โ“ Query Parameters: Full support for URL query strings and hash fragments
  • ๐Ÿ”„ Programmatic Navigation: Navigate with goto() function and reactive stores
  • ๐Ÿ“ฑ Browser History: Full back/forward button support with automatic link interception
  • ๐Ÿ—๏ธ SSR Compatible: Works perfectly with SvelteKit and server-side rendering
  • ๐Ÿ“ฆ TypeScript Ready: Fully typed for better developer experience
  • ๐Ÿชถ Lightweight: Zero external dependencies, minimal bundle size

๐Ÿ“ฆ Installation

npm install svelte5-spa-router
# or
yarn add svelte5-spa-router
# or
pnpm add svelte5-spa-router

๐ŸŽฏ Quick Start

Basic Setup

<!-- App.svelte -->
<script>
    import Router from 'svelte5-spa-router/Router.svelte';
    import Link from 'svelte5-spa-router/Link.svelte';
    import { router } from 'svelte5-spa-router';

    import Home from './routes/Home.svelte';
    import About from './routes/About.svelte';
    import UserProfile from './routes/UserProfile.svelte';
    import NotFound from './routes/NotFound.svelte';

    // Setup routes
    router.addRoute('/', Home);
    router.addRoute('/about', About);
    router.addRoute('/user/:id', UserProfile);
    router.setFallback(NotFound);
</script>

<nav>
    <Link href="/">Home</Link>
    <Link href="/about">About</Link>
    <Link href="/user/123">User Profile</Link>
</nav>

<Router />

Available Imports

// Components
import Router from 'svelte5-spa-router/Router.svelte';
import Link from 'svelte5-spa-router/Link.svelte';

// Router instance and functions
import {
    router, // Main router instance
    goto, // Programmatic navigation
    getQueryParam, // Get query parameter
    updateQueryParams // Update query params
} from 'svelte5-spa-router';

// Reactive stores
import {
    currentRoute, // Current route info
    routeParams, // Route parameters
    queryParams, // Query parameters
    hashFragment // Hash fragment
} from 'svelte5-spa-router';

๐Ÿ›ฃ๏ธ Route Setup

Setting Up Routes

import { router } from 'svelte5-spa-router';
import Home from './components/Home.svelte';
import About from './components/About.svelte';
import UserProfile from './components/UserProfile.svelte';
import BlogPost from './components/BlogPost.svelte';
import Search from './components/Search.svelte';
import AdminPanel from './components/AdminPanel.svelte';
import NotFound from './components/NotFound.svelte';

// Static Routes
router.addRoute('/', Home);
router.addRoute('/about', About);

// Dynamic Routes with Parameters
router.addRoute('/user/:id', UserProfile);
router.addRoute('/blog/:slug', BlogPost);
router.addRoute('/category/:type/item/:id', ItemDetail);

// Optional Parameters
router.addRoute('/search/:query?', Search);

// Wildcard Routes
router.addRoute('/admin/*', AdminPanel);

// Set fallback for 404
router.setFallback(NotFound);

๐Ÿงญ Navigation

<script>
    import Link from 'svelte5-spa-router/Link.svelte';
</script>

<Link href="/about">About Us</Link>
<Link href="/user/123">User Profile</Link>
<Link href="/search?q=svelte">Search Svelte</Link>
<Link href="/docs#introduction">Documentation</Link>

Programmatic Navigation

import { goto } from 'svelte5-spa-router';

// Simple navigation
goto('/about');

// With query parameters
goto('/search', { q: 'svelte', page: '1' });

// With hash fragment
goto('/docs', {}, 'introduction');

// Combined
goto('/search', { q: 'svelte', category: 'frontend' }, 'results');

๐Ÿ“Š Accessing Route Data

Route Parameters

<script>
    import { routeParams } from 'svelte5-spa-router';

    // Access route parameters reactively
    const userId = $derived($routeParams.id);
    const allParams = $derived($routeParams);
</script>

<h1>User Profile: {userId}</h1><p>All params: {JSON.stringify(allParams)}</p>

Query Parameters

<script>
    import { queryParams, getQueryParam, updateQueryParams } from 'svelte5-spa-router';

    // Get single parameter with default
    const searchQuery = $derived(getQueryParam('q', ''));

    // Get all parameters
    const allQueryParams = $derived($queryParams);

    // Update query parameters
    function updateSearch(newQuery) {
        updateQueryParams({ q: newQuery });
    }

    // Replace all query parameters
    function setFilters() {
        updateQueryParams({ category: 'tech', sort: 'date' }, true);
    }
</script>

<input bind:value={searchQuery} onchange={() => updateSearch(searchQuery)} />
<p>Current query: {searchQuery}</p>
<p>All params: {JSON.stringify(allQueryParams)}</p>

Hash Fragments

<script>
    import { hashFragment } from 'svelte5-spa-router';

    const currentHash = $derived($hashFragment);
</script>

<p>Current hash: {currentHash}</p>

๐Ÿ”ง API Reference

Components

Components

<Router>

Main router component that renders the current route based on the URL.

Usage:

<script>
    import Router from 'svelte5-spa-router/Router.svelte';
    import { router } from 'svelte5-spa-router';

    // Setup your routes first
    router.addRoute('/', HomeComponent);
    router.setFallback(NotFoundComponent);
</script>

<Router />

Link component with automatic active state handling and proper navigation.

Props:

  • href (string): Target URL
  • class (string, optional): CSS class for the link

Usage:

<script>
    import Link from 'svelte5-spa-router/Link.svelte';
</script>

<Link href="/about" class="nav-link">About</Link>

Functions

goto(path, queryParams?, hash?)

Navigate programmatically.

  • path: Target path
  • queryParams: Object of query parameters
  • hash: Hash fragment

getQueryParam(key, defaultValue?)

Get a specific query parameter.

updateQueryParams(params, replace?)

Update URL query parameters without navigation.

Stores

All stores are reactive and can be used with $ syntax:

  • currentRoute: Current route information { path, component, params }
  • routeParams: Parameters from current route
  • queryParams: Current query parameters object
  • hashFragment: Current hash fragment string
<script>
    import Link from 'svelte5-spa-router/Link.svelte';
</script>

<Link href="/" class="nav-link">Home</Link>

<style>
    :global(.nav-link) {
        text-decoration: none;
        color: #007acc;
        padding: 0.5rem 1rem;
        border-radius: 4px;
        transition: background-color 0.2s;
    }

    :global(.nav-link:hover) {
        background-color: #f0f0f0;
    }
</style>

๐Ÿ”’ Route Guards (Custom Implementation)

<!-- App.svelte -->
<script>
    import Router from 'svelte5-spa-router/Router.svelte';
    import { currentRoute, goto, router } from 'svelte5-spa-router';

    const protectedRoutes = ['/dashboard', '/profile'];

    // Route guard
    $effect(() => {
        if ($currentRoute && protectedRoutes.includes($currentRoute.path)) {
            if (!isAuthenticated()) {
                goto('/login');
            }
        }
    });

    function isAuthenticated() {
        // Your authentication logic
        return localStorage.getItem('token') !== null;
    }
</script>

<Router />

๐Ÿ”’ Route Guards: Auth, Async, Role-based (beforeEnter)

Svelte5 SPA Router supports function-based route guards using beforeEnter on each route. You can create guards for authentication, async checks, or role-based access (admin/user).

Contoh Penggunaan

<script>
    import Router from 'svelte5-spa-router/Router.svelte';
    import Link from 'svelte5-spa-router/Link.svelte';
    import { goto } from 'svelte5-spa-router';
    import ProtectedPage from './ProtectedPage.svelte';
    import AdminPanel from './AdminPanel.svelte';
    import Home from './Home.svelte';

    // Guard: hanya user login
    function authGuard(to, from) {
        const isAuthenticated = localStorage.getItem('user') !== null;
        if (!isAuthenticated) {
            alert('Access denied! Please login first.');
            return false;
        }
        return true;
    }

    // Guard: async (misal cek token ke server)
    async function asyncAuthGuard(to, from) {
        return new Promise((resolve) => {
            setTimeout(() => {
                const isAuthenticated = localStorage.getItem('user') !== null;
                resolve(isAuthenticated);
            }, 300);
        });
    }

    // Guard: hanya admin
    function roleGuard(to, from) {
        const user = JSON.parse(localStorage.getItem('user') || '{}');
        if (user.role !== 'admin') {
            alert('Only admin can access this route!');
            return false;
        }
        return true;
    }

    const routes = [
        { path: '/', component: Home },
        { path: '/protected', component: ProtectedPage, beforeEnter: authGuard },
        { path: '/admin/:id?', component: ProtectedPage, beforeEnter: asyncAuthGuard },
        { path: '/admin-panel', component: AdminPanel, beforeEnter: roleGuard }
    ];

    function simulateLogin(role = 'user') {
        const name = role === 'admin' ? 'John Admin' : 'Jane User';
        localStorage.setItem('user', JSON.stringify({ name, role }));
        alert(`Login as ${role} successful!`);
    }
    function simulateLogout() {
        localStorage.removeItem('user');
        alert('Logged out!');
        goto('/');
    }
</script>

<div>
    <button on:click={() => simulateLogin('admin')}>Login as Admin</button>
    <button on:click={() => simulateLogin('user')}>Login as User</button>
    <button on:click={simulateLogout}>Logout</button>
    <nav>
        <Link href="/">Home</Link>
        <Link href="/protected">Protected</Link>
        <Link href="/admin/123">Admin Async</Link>
        <Link href="/admin-panel">Admin Panel</Link>
    </nav>
    <Router {routes} />
</div>

// ProtectedPage.svelte dan AdminPanel.svelte bisa berupa halaman biasa.

Penjelasan

Explanation

beforeEnter: Function (sync/async) executed before accessing the route. Return true to proceed, false to block. authGuard: Only logged-in users can access. asyncAuthGuard: Example of async guard (e.g., check token to server). roleGuard: Only users with role: 'admin' can access. Simulate login/logout using localStorage.

Demo Result

Login as user: Can access /protected and /admin/123, cannot access /admin-panel. Login as admin: All routes can be accessed. See the file src/routes/demo.svelte for a complete demo.

Lihat file src/routes/demo.svelte untuk demo lengkap.

๐Ÿงช Testing

// vitest example
import { render, fireEvent } from '@testing-library/svelte';
import { goto, router } from 'svelte5-spa-router';
import Home from '../components/Home.svelte';
import About from '../components/About.svelte';
import App from '../App.svelte';

beforeEach(() => {
    // Setup routes for testing
    router.clearRoutes();
    router.addRoute('/', Home);
    router.addRoute('/about', About);
});

test('should navigate to about page', async () => {
    const { getByText } = render(App);

    await fireEvent.click(getByText('About'));
    expect(getByText('About Page')).toBeInTheDocument();
});

test('should handle dynamic routes', async () => {
    router.addRoute('/user/:id', UserProfile);
    goto('/user/123');

    const { getByText } = render(App);
    expect(getByText('User ID: 123')).toBeInTheDocument();
});

๐Ÿ”„ Migration from Other Routers

From svelte-spa-router

- import router from 'svelte-spa-router'
+ import Router from 'svelte5-spa-router/Router.svelte'
+ import { router } from 'svelte5-spa-router'

- <Router {routes} />
+ // Setup routes first
+ router.addRoute('/', HomeComponent);
+ router.setFallback(NotFoundComponent);
+ <Router />

From @roxi/routify

- import { router } from '@roxi/routify'
+ import { goto } from 'svelte5-spa-router'

- $router.goto('/path')
+ goto('/path')

๐Ÿ—๏ธ SvelteKit Integration

This router works perfectly with SvelteKit for client-side routing:

<!-- src/app.html or main component -->
<script>
    import Router from 'svelte5-spa-router/Router.svelte';
    import { router } from 'svelte5-spa-router';
    import Home from './routes/Home.svelte';
    import About from './routes/About.svelte';
    import NotFound from './routes/NotFound.svelte';

    // Setup routes
    router.addRoute('/', Home);
    router.addRoute('/about', About);
    router.setFallback(NotFound);
</script>

<Router />

Cypress E2E Testing

This project includes comprehensive Cypress end-to-end tests for all SPA router routes, including nested, parameterized, and guarded routes.

Tested Routes

  • / (Home)
  • /about (About)
  • /blog (Blog)
  • /blog/123 (BlogPost)
  • /search?query=router (Search)
  • /user/tanto (UserProfile)
  • /admin-panel (Admin Panel, requires authentication)
  • /multi/123 (MultiParent)
  • /multi/123/child/abc (MultiChild)
  • /multi/123/child/abc/grandchild/foo (MultiGrandchild)
  • /nested (NestedParent)
  • /nested/child (NestedChild)
  • Unknown route (NotFound)

Example: Nested & Multi-level Routes

You can define deeply nested and multi-level routes using flat path patterns:

import MultiParent from './MultiParent.svelte';
import MultiChild from './MultiChild.svelte';
import MultiGrandchild from './MultiGrandchild.svelte';
import NestedParent from './NestedParent.svelte';
import NestedChild from './NestedChild.svelte';

const routes = [
    { path: '/multi/:parentId', component: MultiParent },
    { path: '/multi/:parentId/child/:childId', component: MultiChild },
    { path: '/multi/:parentId/child/:childId/grandchild/:grandId', component: MultiGrandchild },
    { path: '/nested', component: NestedParent },
    { path: '/nested/child', component: NestedChild }
];

// Usage in Svelte:
<Router {routes} />

// Access params in your component:
<script>
    import { routeParams } from 'svelte5-spa-router';
    // $routeParams.parentId, $routeParams.childId, $routeParams.grandId
</script>

Navigation example:

<Link href="/multi/123">MultiParent</Link>
<Link href="/multi/123/child/abc">MultiChild</Link>
<Link href="/multi/123/child/abc/grandchild/foo">MultiGrandchild</Link>
<Link href="/nested">NestedParent</Link>
<Link href="/nested/child">NestedChild</Link>

Admin Route Guard

The /admin-panel route is protected by a role-based guard. Cypress sets the required user object in localStorage before navigation:

cy.visit('http://localhost:5174/admin-panel', {
    onBeforeLoad(win) {
        win.localStorage.setItem('user', JSON.stringify({ role: 'admin', name: 'cypress' }));
    }
});

Running Cypress Tests

  1. Start the dev server:
    npm run dev
    
  2. Run Cypress in interactive mode:
    npx cypress open
    
    Or run all tests headlessly:
    npx cypress run --spec cypress/e2e/routes.integration.cy.js
    

All tests are located in cypress/e2e/routes.integration.cy.js.

๐Ÿ› Troubleshooting

  1. Make sure to import Link like this:
    import Link from 'svelte5-spa-router/Link.svelte';
    
  2. Use <Link href="/about">About</Link>, not a regular <a> tag.
  3. Ensure there are no other elements (overlay/z-index) covering the Link.
  4. Check the browser console for JS errors.

SSR Issues

Make sure you're importing from the correct path and the router handles SSR automatically.

Route Not Matching

Check your route patterns and ensure they match the URL structure exactly.

Ensure you're using the Link component and not regular <a> tags.

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

  • Built for the amazing Svelte 5 and its new runes system
  • Inspired by various SPA routers in the ecosystem
  • Thanks to the Svelte community for feedback and suggestions

Made with โค๏ธ for the Svelte community

Report Bug โ€ข Request Feature โ€ข Documentation

Developing

Once you've created a project and installed dependencies with npm install (or pnpm install or yarn), start a development server:

npm run dev

# or start the server and open the app in a new browser tab
npm run dev -- --open

Everything inside src/lib is part of your library, everything inside src/routes can be used as a showcase or preview app.

Building

To build your library:

npm run package

To create a production version of your showcase app:

npm run build

You can preview the production build with npm run preview.

To deploy your app, you may need to install an adapter for your target environment.

Publishing

Go into the package.json and give your package the desired name through the "name" option. Also consider adding a "license" field and point it to a LICENSE file which you can create from a template (one popular option is the MIT license).

To publish your library to npm:

npm publish

Top categories

Loading Svelte Themes