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-routing fails to compile its source. Authoring our own router and shipping pre-compiled JS sidesteps the entire problem.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:
// 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):
// 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:
<!-- 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.
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 state — page.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.
window.location directly.<Router basepath> blocks for nested routers.'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.
:param, :param? (optional), and * (wildcard) syntax.params argument typed from the path literal — no annotation needed.params is type-inferred from the path literal — no annotation needed:
<!-- :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):
/users/me beats /users/:id)./a/b/:c beats /a/:b/:c).#<Link>
Anchor wrapper with click interception, prefetch, and programmatic-nav semantics. Renders a real <a href>:
<!-- <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>'hover' (default), 'viewport', or 'none'.history.replaceState instead of pushState.goto().All standard <a> attributes pass through (class, aria-*, etc.).
Behavior:
goto().target="_blank", download, modifier-key clicks (Ctrl/Cmd/Shift): fall through to browser default. No interception.<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.
// 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:
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:
<!-- 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:
<!-- 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.