svelte-router: A Lightweight Svelte 3–5 Router Without SvelteKit
@shaun/svelte-router is a lightweight Svelte 5 router that works from Svelte 3 through Svelte 5 — no SvelteKit required. If you are building a SPA with Vite, it gives you declarative links, dynamic params, wildcard routes, and hash or HTML5 history modes in a dependency-free package.
The latest 2.x line adds full Svelte 5 support while keeping the same minimal API.
Install
npm install @shaun/svelte-router
Works with npm, bun and yarn. The only peer dependency is svelte (^3 || ^4 || ^5).
Quick start
<script>
import { createRouter, link, Link, View } from '@shaun/svelte-router'
import Home from './Home.svelte'
import User from './User.svelte'
import NotFound from './NotFound.svelte'
const routes = [
{ path: '/', component: Home },
{ path: '/users/:userId(\\d+)', component: User },
{ path: '*', component: NotFound }
]
const router = createRouter({ routes })
</script>
<Link href="/">Home</Link>
<Link href="/users/123">Someone</Link>
<a use:link href="/users/111">a link with action</a>
<View></View>
Route params are passed straight to your component as props — export let userId just works.
Highlights
- Dynamic params with custom regex —
/users/:userId(\d+)only matches numeric ids, so/:productNamecan stay a catch-all for everything else. Declaration order decides precedence: put more specific routes first. - Wildcard routes —
{ path: '*', component: NotFound }for 404 handling. - Active link states —
activeClass/exactActiveClassprops (defaults:active/exact-active). Child paths like/users/123count as active for/users, but/users2does not. - Programmatic navigation —
router.push('/users', { page: 2 })androuter.replace(...), with query-string merging built in. - Base path support — deploy under a subfolder with
base: '/demo'andvite build --base=/demo/. - Two history modes —
hashfor static hosts,web(HTML5) for clean URLs. - Zero runtime dependencies — just Svelte.
HTML5 mode server setup
With web mode, direct visits to nested paths need a catch-all fallback so the app can handle the route. nginx:
location / {
try_files $uri $uri/ /index.html;
}
Apache:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Caddy v2:
try_files {path} /
Performance
From the local matcher microbenchmark on Node.js v26 (4000+ routes):
| Case | ops/sec | us/op |
|---|---|---|
| match static | 28,051,797 | 0.036 |
| match dynamic first | 1,482,711 | 0.674 |
| match dynamic last | 1,161,006 | 0.861 |
| match no dynamic bucket | 12,001,774 | 0.083 |
| match no keyed dynamic | 7,300,384 | 0.137 |
Static routes are matched directly. Dynamic routes are indexed by segment count and first static segment, with wildcard-first dynamic routes used as a fallback while preserving declaration order.
Why not just use svelte-spa-router?
svelte-spa-router is a solid choice and very popular — it is hash-first by design. @shaun/svelte-router is for when you want HTML5 history mode as the default, regex-constrained params, route-order-first matching, and a router with zero runtime dependencies. Both are MIT licensed; pick whichever fits your deployment.
Related projects
I also maintain a client-side router for Alpine.js. See all my open-source projects on the Projects page.
Project links
- npm: @shaun/svelte-router
- GitHub: shaunlee/svelte-router
- License: MIT
If you are building a Svelte SPA without SvelteKit, give it a try. Issues and pull requests are welcome.