@real-router/svelte
v0.10.1
Published
Svelte 5 integration for Real-Router
Maintainers
Readme
@real-router/svelte
Svelte 5 integration for Real-Router — composables, components, and context providers.
Installation
npm install @real-router/svelte @real-router/core @real-router/browser-pluginPeer dependency: svelte >= 5.7.0
Quick Start
<!-- App.svelte -->
<script lang="ts">
import { createRouter } from "@real-router/core";
import { browserPluginFactory } from "@real-router/browser-plugin";
import { RouterProvider, RouteView, Link } from "@real-router/svelte";
import HomePage from "./HomePage.svelte";
import UsersPage from "./UsersPage.svelte";
import NotFoundPage from "./NotFoundPage.svelte";
const router = createRouter([
{ name: "home", path: "/" },
{
name: "users",
path: "/users",
children: [{ name: "profile", path: "/:id" }],
},
]);
router.usePlugin(browserPluginFactory());
router.start();
</script>
<RouterProvider {router}>
<nav>
<Link routeName="home">Home</Link>
<Link routeName="users">Users</Link>
</nav>
<RouteView nodeName="">
{#snippet home()}
<HomePage />
{/snippet}
{#snippet users()}
<UsersPage />
{/snippet}
{#snippet notFound()}
<NotFoundPage />
{/snippet}
</RouteView>
</RouterProvider>Composables
All composables must be called during component initialization (not inside $effect or event handlers). Reactive composables return { current: T } getter objects — read .current inside a template or $derived to register a reactive dependency.
| Composable | Returns | Reactive? |
| ----------------------- | --------------------------------------------------------------- | ------------------------------------------ |
| useRouter() | Router | Never |
| useNavigator() | Navigator | Never (stable ref, safe to use directly) |
| useRoute() | { navigator, route: { current }, previousRoute: { current } } | .current on every navigation |
| useRouteNode(name) | { navigator, route: { current }, previousRoute: { current } } | .current when node activates/deactivates |
| useRouteUtils() | RouteUtils | Never |
| useRouterTransition() | { current: RouterTransitionSnapshot } | .current on transition start/end |
| useRouteExit(handler, options?) | void — wraps subscribeLeave with abort + same-route guards | Never (handler captured at init) |
| useRouteEnter(handler, options?) | void — fires once on nav-driven mount via $effect + transition.from | Never (handler captured at init) |
<!-- useRouteNode — updates only when "users.*" changes -->
<script lang="ts">
import { useRouteNode } from "@real-router/svelte";
const { route } = useRouteNode("users");
</script>
{#if route.current}
{#if route.current.name === "users"}
<UsersList />
{:else if route.current.name === "users.profile"}
<UserProfile id={route.current.params.id} />
{/if}
{/if}<!-- useNavigator — stable reference, never reactive -->
<script lang="ts">
import { useNavigator } from "@real-router/svelte";
const navigator = useNavigator();
</script>
<button onclick={() => navigator.navigate("home")}>Back</button><!-- useRouterTransition — progress bars, loading states -->
<script lang="ts">
import { useRouterTransition } from "@real-router/svelte";
const transition = useRouterTransition();
</script>
{#if transition.current.isTransitioning}
<div class="progress-bar"></div>
{/if}<!-- useRouteExit — exit animations, draft autosave, AbortSignal-aware cleanup -->
<script lang="ts">
import { useRouteExit } from "@real-router/svelte";
let el: HTMLDivElement;
useRouteExit(async ({ signal }) => {
if (!el) return;
el.classList.add("fade-out");
const cleanup = () => el.classList.remove("fade-out");
signal.addEventListener("abort", cleanup, { once: true });
el.getBoundingClientRect(); // style flush
await Promise.allSettled(el.getAnimations().map((a) => a.finished));
cleanup();
});
</script>
<div bind:this={el}>...</div><!-- useRouteEnter — page-enter analytics, focus management, entry animations -->
<script lang="ts">
import { useRouteEnter } from "@real-router/svelte";
useRouteEnter(({ route, previousRoute }) => {
analytics.track("page_enter", {
route: route.name,
from: previousRoute.name,
});
});
</script>Svelte handler-reactivity: composables run once at init, so
handleris captured at hook-call time. To vary behavior over time, read$state/$derivedinside the handler body. See CLAUDE.md → "useRouteExit / useRouteEnter Handler Is Captured At Init".
Components
<Link>
Navigation link with automatic active state detection. Uses $derived for href and class — only the DOM attributes update when active state changes.
<Link
routeName="users.profile"
routeParams={{ id: "123" }}
activeClassName="active"
activeStrict={false}
ignoreQueryParams={true}
routeOptions={{ replace: true }}
>
View Profile
</Link>Props:
| Prop | Type | Default | Description |
| ------------------- | ------------------- | ----------- | --------------------------------------- |
| routeName | string | required | Target route name |
| routeParams | Params | {} | Route parameters |
| routeOptions | NavigationOptions | {} | Navigation options (replace, etc.) |
| class | string | undefined | CSS class |
| activeClassName | string | "active" | Class added when route is active |
| activeStrict | boolean | false | Exact match only (no ancestor matching) |
| ignoreQueryParams | boolean | true | Query params don't affect active state |
| hash | string | undefined | URL fragment (decoded). Tri-state: undefined preserves, "" clears, value sets. (#532) |
| target | string | undefined | Link target (_blank, etc.) |
| onclick | (evt: MouseEvent) => void | undefined | Custom click handler. Runs before the navigation logic — call evt.preventDefault() to suppress navigation. |
All other props are spread onto the <a> element.
hash — URL fragment / tab-style UIs
<Link routeName="settings" hash="profile">Profile</Link>
<Link routeName="settings" hash="account">Account</Link>Active class is hash-aware — only the matching tab lights up. Live demo: examples/web/react/link-hash/ — behavior is identical across adapters, only template syntax differs. See the Hash Fragment Support wiki page for the full surface.
<Lazy>
Lazy-load route content with a fallback component while loading. Useful for code-splitting and dynamic imports.
<RouteView nodeName="">
{#snippet dashboard()}
<Lazy loader={() => import('./Dashboard.svelte')} fallback={Spinner} />
{/snippet}
</RouteView>Props:
| Prop | Type | Default | Description |
| ---------- | --------------------------------------- | ----------- | ----------------------------------------- |
| loader | () => Promise<{ default: Component }> | required | Async function that imports the component |
| fallback | Component | undefined | Component to render while loading |
The loader function should return a dynamic import promise. The fallback component is rendered while the import is pending. If an error occurs during loading, an error message is displayed.
<RouteView>
Declarative route matching. Renders the snippet whose name matches the active route segment.
<RouteView nodeName="">
{#snippet users()}
<UsersPage />
{/snippet}
{#snippet settings()}
<SettingsPage />
{/snippet}
{#snippet notFound()}
<NotFoundPage />
{/snippet}
</RouteView>Props:
| Prop | Type | Description |
| ----------- | --------- | ------------------------------------------- |
| nodeName | string | Route node to match against. "" for root. |
| notFound | Snippet | Rendered when route is UNKNOWN_ROUTE |
| [segment] | Snippet | Named snippet matching a route segment |
Snippet names must be valid JavaScript identifiers and match the first segment of the active route after nodeName. For a route users.profile with nodeName="", the snippet named users matches.
Note:
keepAliveis not supported. Svelte has no equivalent of React's<Activity>API or Vue's<KeepAlive>. Components are destroyed when navigating away.
<RouterErrorBoundary>
Declarative error handling for navigation errors. Shows a fallback alongside children (not instead of) when a guard rejects or a route is not found.
<script lang="ts">
import { RouterErrorBoundary } from "@real-router/svelte";
</script>
<RouterErrorBoundary
onError={(error, toRoute, fromRoute) =>
analytics.track("nav_error", {
code: error.code,
to: toRoute?.name,
from: fromRoute?.name,
})
}
>
{#snippet fallback(error, resetError)}
<div class="toast">
{error.code} <button onclick={resetError}>Dismiss</button>
</div>
{/snippet}
<Link routeName="protected">Go to Protected</Link>
</RouterErrorBoundary>Auto-resets on next successful navigation. Works with both <Link> and imperative router.navigate().
onError signature: (error, toRoute, fromRoute) => void. Receives the RouterError, the attempted destination (State | null), and the previously active route (State | null). A throwing onError is caught by the boundary, logged via console.error, and never breaks reactivity.
Actions
createLinkAction
Factory function that creates a low-level action for adding navigation to any element. Must be called during component initialization to capture the router context.
<script lang="ts">
import { createLinkAction } from "@real-router/svelte";
const link = createLinkAction();
</script>
<a use:link={{ name: "users.profile", params: { id: "123" } }}>
User Profile
</a>
<button use:link={{ name: "home" }}>
Go Home
</button>
<div use:link={{ name: "settings", params: {}, options: { replace: true } }} role="link" tabindex="0">
Settings
</div>Parameters:
| Property | Type | Default | Description |
| --------- | -------- | ------- | ---------------------------------- |
| name | string | — | Target route name |
| params | Params | {} | Route parameters |
| options | object | {} | Navigation options (replace, etc.) |
The action automatically adds role="link" + tabindex="0" to non-interactive elements for accessibility. It handles click events and Enter key navigation.
Reactive Primitives
createReactiveSource
Public building block that bridges any RouterSource<T> to Svelte's reactivity system. Returns a { current: T } getter object that lazily subscribes via createSubscriber.
<script lang="ts">
import { createReactiveSource, useRouter } from "@real-router/svelte";
import { createActiveRouteSource } from "@real-router/sources";
const router = useRouter();
const isActive = createReactiveSource(
createActiveRouteSource(router, "users.profile", {})
);
</script>
{#if isActive.current}
<span class="badge">Active</span>
{/if}Use cases: custom active route indicators, domain-specific composables, integration with other reactive primitives.
Svelte-Specific Patterns
Reading .current in Reactive Contexts
Unlike Vue (ShallowRefs) or Solid (Accessors), Svelte composables return { current: T } getter objects. Read .current inside a template or $derived to register a reactive dependency:
<script lang="ts">
import { useRoute } from "@real-router/svelte";
const { route } = useRoute();
// CORRECT — $derived registers a reactive dependency
const routeName = $derived(route.current?.name);
// WRONG — read outside reactive context, no subscription
console.log(route.current?.name);
</script>
<!-- CORRECT — template is a reactive context -->
<p>{route.current?.name}</p>Reacting to Route Changes
Use $effect to run side effects when the route changes:
<script lang="ts">
import { useRouteNode } from "@real-router/svelte";
const { route } = useRouteNode("users");
$effect(() => {
if (route.current) {
document.title = `Users — ${route.current.params.id ?? "list"}`;
}
});
</script>Nested RouteView
For nested routes, use RouteView at each level with the appropriate nodeName:
<!-- Top-level: matches "users", "settings", etc. -->
<RouteView nodeName="">
{#snippet users()}
<!-- Nested: matches "users.list", "users.profile", etc. -->
<RouteView nodeName="users">
{#snippet list()}
<UsersList />
{/snippet}
{#snippet profile()}
<UserProfile />
{/snippet}
</RouteView>
{/snippet}
</RouteView>Accessibility
Enable screen reader announcements for route changes:
<RouterProvider {router} announceNavigation>
{/* Your app */}
</RouterProvider>When enabled, a visually hidden aria-live region announces each navigation. Focus moves to the first <h1> on the new page. See Accessibility guide for details.
Scroll Restoration
Opt-in preservation of scroll position across navigations:
<RouterProvider {router} scrollRestoration={{ mode: "restore" }}>
<!-- Your app -->
</RouterProvider>Restores scroll on back/forward, scrolls to top (or #hash) on push. Three modes: "restore" (default), "top", "native". Custom containers via scrollContainer: () => HTMLElement | null. Lifecycle tied to the provider — created on mount, destroyed on unmount. See Scroll Restoration guide for details.
View Transitions
Opt-in animated route transitions via the browser's View Transitions API:
<RouterProvider {router} viewTransitions>
<!-- Your app -->
</RouterProvider>Reactive via $effect — toggling the prop creates/destroys the utility. No-op on unsupported browsers (Firefox as of 2026-04, SSR). Customization is pure CSS via ::view-transition-* pseudo-elements and view-transition-name for hero morphs. See View Transitions guide for patterns.
Documentation
Full documentation: Wiki
- RouterProvider · RouteView · RouterErrorBoundary · Link · Scroll Restoration · View Transitions
- useRouter · useRoute · useRouteNode · useNavigator · useRouteUtils · useRouterTransition · useRouteExit · useRouteEnter
Examples
16 runnable examples — each is a standalone Vite app. Run: cd examples/web/svelte/basic && pnpm dev
basic · nested-routes · auth-guards · data-loading · lazy-loading · async-guards · hash-routing · persistent-params · error-handling · dynamic-routes · link-action · lazy-loading-svelte · snippets-routing · reactive-source · search-schema · combined
Related Packages
| Package | Description |
| ---------------------------------------------------------------------------------------- | ------------------------------------ |
| @real-router/core | Core router (required dependency) |
| @real-router/browser-plugin | Browser History API integration |
| @real-router/sources | Subscription layer (used internally) |
| @real-router/route-utils | Route tree queries (useRouteUtils) |
Contributing
See contributing guidelines for development setup and PR process.
