AbsoluteJS

Svelte SPA

Drive client-side sub-route navigation inside a Svelte page using the AbsoluteJS-shipped router. Unlike React, Vue, and Angular — which have battle-tested third-party or first-party routers — Svelte's ecosystem doesn't have a canonical answer, so AbsoluteJS ships one as a sub-export.

#Why AbsoluteJS Ships This

Svelte has no first-party router.SvelteKit is a meta-framework, not a router — and it owns the whole app. AbsoluteJS users want a router that drops into a single page, not a runtime that takes over.
Third-party routers ship raw .svelte files.Bun has no built-in Svelte plugin, so a server bundle pass over something like svelte-routing fails to compile its source. Authoring our own router and shipping pre-compiled JS sidesteps the entire problem.
API names align with SvelteKitwhere there's an analog (goto, page, pushState, replaceState) so users migrating from SvelteKit don't relearn the primitives.

#Wildcard Route

Refresh on any sub-route hits the server with the actual URL. Register a wildcard pattern so the same handler responds for every URL the page's router knows about:

TS
// backend/server.ts — register a wildcard route per page so refresh /
// deep-link on any sub-route hits the same handler. The native router
// then dispatches based on the URL.
new Elysia()
  .get('/dashboard', dashboardHandler)
  .get('/dashboard/*', dashboardHandler)
  .listen(3000);

#Page Handler

Pass request through to the page handler. The handler computes new URL(request.url).pathname and merges it into your page's props as url (only if you didn't supply your own url):

TS
// backend/server.ts
import { handleSveltePageRequest } from '@absolutejs/absolute/svelte';
import type SvelteSpa from '../frontend/svelte/pages/SvelteSpa.svelte';

const svelteHandler = ({ request }: { request: Request }) =>
  handleSveltePageRequest<typeof SvelteSpa>({
    indexPath: asset(manifest, 'SvelteSpaIndex'),
    pagePath: asset(manifest, 'SvelteSpa'),
    props: { cssPath: asset(manifest, 'SpaCSS') },
    request, // ← URL auto-injected as props.url for the page
  });

new Elysia()
  .get('/svelte', svelteHandler)
  .get('/svelte/*', svelteHandler);

#Page Component

Wrap your routes in <Router url={url}>. The url prop drives SSR matching; on the client it's undefined and the router reads window.location:

SVELTE
<!-- frontend/svelte/pages/SvelteSpa.svelte -->
<script lang="ts">
  import Link from '@absolutejs/absolute/svelte/router/Link.svelte';
  import Route from '@absolutejs/absolute/svelte/router/Route.svelte';
  import Router from '@absolutejs/absolute/svelte/router/Router.svelte';
  import { page } from '@absolutejs/absolute/svelte/router';

  type Props = { url?: string; cssPath?: string };
  let { url, cssPath }: Props = $props();
</script>

<Router {url}>
  <nav>
    <Link to="/svelte">Home</Link>
    <Link to="/svelte/settings">Settings</Link>
    <Link to="/svelte/profile">Profile</Link>
  </nav>

  <Route path="/svelte">
    {#snippet content()}<h2>Home</h2>{/snippet}
  </Route>
  <Route path="/svelte/settings">
    {#snippet content()}<h2>Settings</h2>{/snippet}
  </Route>
  <Route path="/svelte/profile">
    {#snippet content()}<h2>Profile</h2>{/snippet}
  </Route>

  <p>Current path: {page.url.pathname}</p>
</Router>

#Primitives

Three components and four runtime primitives, exported from @absolutejs/absolute/svelte/router. Components are imported by file path so Svelte's compiler resolves them; everything else is a regular package-root import.

TS
import {
  // Components (import each from its file path):
  // <Router>  — context provider, owns the registry + match resolver
  // <Route>   — declarative path matcher, renders its snippet at its
  //             own DOM location when active
  // <Link>    — anchor wrapper with click interception + prefetch
  // ↑ These three are available as 'svelte/router/Router.svelte' etc.

  // Programmatic API (importable from the package root):
  goto,           // (url, opts?) — programmatic nav, runs View Transition
  pushState,      // (url, state) — shallow routing (no Route swap)
  replaceState,   // (url, state) — same but with history.replaceState
  page,           // reactive route statepage.url, page.params, page.state

  // Types:
  type GotoOptions,
  type PageState,
  type RouterMode,
  type LinkPrefetchMode,
  type ExtractRouteParams,
} from '@absolutejs/absolute/svelte/router';

#<Router>

Context provider that owns the route registry and resolves the active match. Wrap your page (or any subtree) in <Router url={url}> on the server, <Router> on the client.

urlSSR URL passthrough. On the server, the page handler auto-injects the request pathname into props (see Page Handler section below). On the client, omit it — the router reads window.location directly.
basepathoptional URL prefix the router operates under. Stacks with parent <Router basepath> blocks for nested routers.
mode'history' (default, clean URLs) or 'hash' (/#/path, for static deploys).

#<Route>

Declarative path matcher. Renders its content snippet at its own DOM location when active — so a<Route> nested inside a layout <section> renders inside that section, not at the router's root.

pathpattern with :param, :param? (optional), and * (wildcard) syntax.
contenta Svelte 5 snippet. Receives a params argument typed from the path literal — no annotation needed.

params is type-inferred from the path literal — no annotation needed:

SVELTE
<!-- :param syntax — params type is INFERRED from the path literal -->
<Route path="/users/:id">
  {#snippet content(params)}
    <!-- params is { id: string } — fully typed, no annotation -->
    <p>User #{params.id}</p>
  {/snippet}
</Route>

<Route path="/users/:id/posts/:postId?">
  {#snippet content(params)}
    <!-- params is { id: string; postId: string | undefined } -->
    <p>User {params.id}, post {params.postId ?? '(none)'}</p>
  {/snippet}
</Route>

<Route path="/files/*">
  {#snippet content(params)}
    <!-- params is { wildcard: string } -->
    <p>File path: {params.wildcard}</p>
  {/snippet}
</Route>

Match priority (specificity ranking, matches React Router v6+, Vue Router, Angular Router, and SvelteKit's internal matcher):

1
Longest static prefix wins
(/users/me beats /users/:id).
2
Then most static segments
(/a/b/:c beats /a/:b/:c).
3
Then declaration order (tie-breaker).

Anchor wrapper with click interception, prefetch, and programmatic-nav semantics. Renders a real <a href>:

SVELTE
<!-- <Link> renders a real <a href> for progressive enhancement.
     Click is intercepted on same-origin URLs (modifier-key clicks,
     target=_blank, download, and external URLs all fall through to
     a normal full-page navigation). -->

<Link to="/settings">Settings</Link>

<!-- Hover prefetch is on by default; viewport / off are alternatives. -->
<Link to="/profile" prefetch="viewport">Profile</Link>
<Link to="/contact" prefetch="none">Contact</Link>

<!-- Use replaceState instead of pushState (matches SvelteKit's API). -->
<Link to="/login" replaceState>Log in</Link>

<!-- All standard <a> attributes pass through. -->
<Link to="/docs" class="btn-primary" aria-label="Open docs">Docs</Link>
todestination URL (relative or absolute).
prefetch'hover' (default), 'viewport', or 'none'.
replaceStateuse history.replaceState instead of pushState.
noScroll, keepFocuspass-through to the underlying goto().

All standard <a> attributes pass through (class, aria-*, etc.).

Behavior:

Internal same-origin URLsclient-side navigation via goto().
External URLstarget="_blank", download, modifier-key clicks (Ctrl/Cmd/Shift): fall through to browser default. No interception.
No JS availablethe rendered <a href> still works as a normal link. Progressive enhancement by default.

#page (rune)

Reactive route state. Mirrors SvelteKit's page from $app/state — same shape, same property names, so SvelteKit code that reads page.url.pathname ports over without changes.

TS
// page is a Svelte 5 $state-backed reactive object. Direct property
// access in templates re-renders. Mirrors SvelteKit's `page` from
// `$app/state` — same shape, same names.
import { page } from '@absolutejs/absolute/svelte/router';

// page.url       — URL object for the current path (reactive)
// page.url.pathname / .search / .hash / .searchParams
// page.params    — Record<string, string | undefined> from active Route
// page.state     — value attached to history.state (set via goto/pushState)

#goto / pushState / replaceState

Programmatic navigation and shallow-routing primitives. Names match SvelteKit's $app/navigation:

TS
import { goto, pushState, replaceState } from '@absolutejs/absolute/svelte/router';

// Programmatic navigation. Triggers View Transitions when supported.
await goto('/dashboard');
await goto('/login', { replaceState: true });
await goto('/users/42', {
  state: { from: 'search' }, // available via page.state
  noScroll: true,
  keepFocus: true,
});

// Shallow routing — update URL bar + page.state without re-running
// <Route> matching. Useful for modals / drawers / side panels that
// want a shareable URL but shouldn't swap the active route.
pushState('/photos/42', { modal: 'photo', id: 42 });
replaceState('/photos/42', { modal: 'photo', id: 42 });

goto() wraps the location change in document.startViewTransition when supported. Reduced-motion users automatically get instant swaps.

#Hash Mode

Opt-in via <Router mode="hash">. Matches against window.location.hash instead ofpathname. Useful for static deploys where the host can't be configured to wildcard-route to a single HTML file:

SVELTE
<!-- Hash mode is opt-in via the mode prop. The matcher operates against
     window.location.hash (with the leading "#/" stripped) instead of
     window.location.pathname. Useful for static deploys (GitHub Pages,
     S3, etc.) where the host can't be configured to wildcard-route to
     a single HTML file. -->

<Router mode="hash" {url}>
  <Route path="/dashboard">
    {#snippet content()}<Dashboard />{/snippet}
  </Route>
</Router>

<!-- URL: https://example.com/#/dashboard -->

#Nested Routers

<Router basepath> values stack from outer to inner. Useful for embeddable SPA fragments shipped as packages:

SVELTE
<!-- Nested <Router> blocks compose their basepaths. The inner router
     matches against the full URL, but its <Route path> values are
     interpreted relative to the stacked basepath. Useful for embeddable
     SPA fragments shipped as packages. -->

<Router basepath="/portal" {url}>
  <Router basepath="/admin">
    <Route path="/users">
      {#snippet content()}<!-- matches /portal/admin/users -->{/snippet}
    </Route>
  </Router>
</Router>

page.url in the inner context still reflects the full URL; only <Route path> matching is scoped by the basepath stack.