svelte5-spa-router
v1.3.0
Published
A simple, flexible, and lightweight SPA router specifically designed for Svelte 5 with runes support (dev/non-stable, not production ready)
Downloads
35
Maintainers
Readme
🆕 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 changeslocationStore 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).
- Flat route definitions for deeply nested paths (e.g.
- Improved Route Guards:
- Role-based, async, and parameterized guards with
beforeEnter.
- Role-based, async, and parameterized guards with
- 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
pathstrings for all routes. - Example:
// Old (nested) { path: '/multi/:parentId', children: [ ... ] } // New (flat) { path: '/multi/:parentId/child/:childId', component: MultiChild }
- Replace nested/children arrays with flat
- Route Guards:
- Use
beforeEnteron each route for sync/async/role checks.
- Use
- Testing:
- Use Cypress for browser-based E2E tests (
cypress/e2e/routes.integration.cy.js).
- Use Cypress for browser-based E2E tests (
- No Breaking Changes:
- Existing array-based config and imperative
router.addRoutestill supported.
- Existing array-based config and imperative
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 toindex.html(see Troubleshooting below).
1. Install
npm install svelte5-spa-router2. Definisikan Route dan Komponen (REKOMENDASI UTAMA)
2. Define Routes and Components (RECOMMENDED)
<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 fromsrc/lib/. - To access params, use
$routeParamsand$queryParamsin 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
Using Link Component
<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>
Link component with automatic active state handling and proper navigation.
Props:
href(string): Target URLclass(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 pathqueryParams: Object of query parametershash: 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 routequeryParams: Current query parameters objecthashFragment: Current hash fragment string
🎨 Styling Links
<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
- Start the dev server:
npm run dev - Run Cypress in interactive mode:
Or run all tests headlessly:npx cypress opennpx cypress run --spec cypress/e2e/routes.integration.cy.js
All tests are located in cypress/e2e/routes.integration.cy.js.
🐛 Troubleshooting
Link Not Working When Clicked
- Make sure to import Link like this:
import Link from 'svelte5-spa-router/Link.svelte'; - Use
<Link href="/about">About</Link>, not a regular<a>tag. - Ensure there are no other elements (overlay/z-index) covering the Link.
- 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.
Active Links Not Working
Ensure you're using the Link component and not regular <a> tags.
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - 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 -- --openEverything 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 packageTo create a production version of your showcase app:
npm run buildYou 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