locationStore
for Layout ReactivityYou 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.
<script>
import { locationStore } from 'svelte5-spa-router';
$: hideSidebar = $locationStore.pathname === '/login';
</script>
{#if !hideSidebar}
<Sidebar />
{/if}
/multi/:parentId/child/:childId/grandchild/:grandId
).beforeEnter
.path
strings for all routes.// Old (nested)
{ path: '/multi/:parentId', children: [ ... ] }
// New (flat)
{ path: '/multi/:parentId/child/:childId', component: MultiChild }
beforeEnter
on each route for sync/async/role checks.cypress/e2e/routes.integration.cy.js
).router.addRoute
still supported.See the updated README and demo for usage patterns and migration examples.
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).
npm install svelte5-spa-router
<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>
router.addRoute
) is only for advanced use-cases (e.g. dynamic/plugin route injection), not needed for most apps.svelte5-spa-router
), not from src/lib/
.$routeParams
and $queryParams
in your template.A simple, flexible, and lightweight SPA router specifically designed for Svelte 5 with runes support.
:id
), optional parameters (:id?
), and wildcards (/*
)goto()
function and reactive storesnpm install svelte5-spa-router
# or
yarn add svelte5-spa-router
# or
pnpm add svelte5-spa-router
<!-- 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 />
// 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';
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);
<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>
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');
<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>
<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>
<script>
import { hashFragment } from 'svelte5-spa-router';
const currentHash = $derived($hashFragment);
</script>
<p>Current hash: {currentHash}</p>
<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 linkUsage:
<script>
import Link from 'svelte5-spa-router/Link.svelte';
</script>
<Link href="/about" class="nav-link">About</Link>
goto(path, queryParams?, hash?)
Navigate programmatically.
path
: Target pathqueryParams
: Object of query parametershash
: Hash fragmentgetQueryParam(key, defaultValue?)
Get a specific query parameter.
updateQueryParams(params, replace?)
Update URL query parameters without navigation.
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<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>
<!-- 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 />
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).
<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.
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.
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.
// 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();
});
- 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 />
- import { router } from '@roxi/routify'
+ import { goto } from 'svelte5-spa-router'
- $router.goto('/path')
+ goto('/path')
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 />
This project includes comprehensive Cypress end-to-end tests for all SPA router routes, including nested, parameterized, and guarded 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)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>
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' }));
}
});
npm run dev
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
.
import Link from 'svelte5-spa-router/Link.svelte';
<Link href="/about">About</Link>
, not a regular <a>
tag.Make sure you're importing from the correct path and the router handles SSR automatically.
Check your route patterns and ensure they match the URL structure exactly.
Ensure you're using the Link
component and not regular <a>
tags.
Contributions are welcome! Please feel free to submit a Pull Request.
git checkout -b feature/AmazingFeature
)git commit -m 'Add some AmazingFeature'
)git push origin feature/AmazingFeature
)This project is licensed under the MIT License - see the LICENSE file for details.
Made with โค๏ธ for the Svelte community
Report Bug โข Request Feature โข Documentation
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.
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.
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