svelte-router: A Lightweight Svelte 3–5 Router Without SvelteKit

· 3 min read

@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 /:productName can 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 statesactiveClass / exactActiveClass props (defaults: active / exact-active). Child paths like /users/123 count as active for /users, but /users2 does not.
  • Programmatic navigationrouter.push('/users', { page: 2 }) and router.replace(...), with query-string merging built in.
  • Base path support — deploy under a subfolder with base: '/demo' and vite build --base=/demo/.
  • Two history modeshash for 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

If you are building a Svelte SPA without SvelteKit, give it a try. Issues and pull requests are welcome.