A powerful Svelte 5 port of React Router - the modern, lightweight, performant, and accessible routing library for single-page applications.
🚧 Work in Progress: This project is actively being developed. Issues, feature requests, and pull requests are welcome! Help us make this the best react-router port for Svelte 5.
Version 0.1.0 requires Svelte 5.57.0 or newer within Svelte 5. Upgrading from
0.0.6 requires migrating value hooks to readonly .current handles; see the
migration contract below.
Install it:
npm i @hvniel/svelte-router
# or
yarn add @hvniel/svelte-router
# or
pnpm add @hvniel/svelte-routerThis library is building toward React Router's declarative- and data-mode behaviour while adapting the API to Svelte 5. It is not yet a complete port; use the React Router parity plan to see what is tested, partial, or missing.
The upstream test audit records the pinned React Router revision, completed counterpart suites, and explicit Svelte adaptations. A passing local suite is not a claim that the entire upstream suite has been ported.
The router supports both data routing and declarative routing modes, giving you flexibility in how you structure your application.
Note on Framework Mode: This port focuses exclusively on data and declarative routing modes for client-side applications. The "framework" mode (equivalent to Remix) is not planned for this library since SvelteKit already provides an exceptional full-stack framework experience for Svelte applications. If you need server-side rendering, file-based routing, or full-stack capabilities, we recommend using SvelteKit instead.
- Quick start
- Routing Modes
- Key Features
- Examples
- Key Differences from React Router
- Core Concepts
- API Reference
- React Router parity plan
- Contributing
Data routing registers route objects with an engine that owns navigation,
loaders/actions, revalidation, and blockers. This example is a client-side SPA
entry; Root.svelte renders an Outlet, and Home.svelte is application UI:
<script lang="ts">
import { onDestroy } from 'svelte';
import { createBrowserRouter, RouterProvider } from '@hvniel/svelte-router';
import Root from './Root.svelte';
import Home from './Home.svelte';
import User, { loader as userLoader } from './User.svelte';
const router = createBrowserRouter([
{
path: "/",
Component: Root,
children: [
{
index: true,
Component: Home,
},
{
path: "users/:id",
Component: User,
loader: userLoader,
},
],
},
]);
// This entry creates and owns the engine, not merely its subscription.
onDestroy(() => router.dispose());
</script>
<RouterProvider {router} />The User.svelte component and loader are shown below. Exporting a loader from a
component does not register it automatically; the loader route property does.
Declarative routing uses familiar component-based syntax for defining routes:
<script>
import { BrowserRouter, Routes, Route } from '@hvniel/svelte-router';
import Home from './Home.svelte';
import About from './About.svelte';
import User from './User.svelte';
</script>
<BrowserRouter>
<Routes>
<Route path="/" Component={Home} />
<Route path="/about" Component={About} />
<Route path="/users/:id" Component={User} />
</Routes>
</BrowserRouter>Both modes are useful. A descendant Routes table can also render under a data
router's splat route, but its definitions do not become engine-managed
loader/action routes. In 0.0.6, useBlocker requires a data router, even if the
application owns its data fetching. See router architecture.
- Multiple Router Types: BrowserRouter, HashRouter, MemoryRouter, and StaticRouter
- Data Loading: Built-in loader and action functions for data fetching
- Nested Routing: Nested routes and layouts through
Outlet - Native HTML: Links and forms enhance ordinary elements; a client-only app still requires JavaScript
- TypeScript: Public route/data/controller types checked against the emitted package with
skipLibCheck: false - Svelte 5: Components, snippets, runes, and live readonly
.currentvalue handles - Non-navigation operations: Keyed fetchers with engine-owned cancellation and revalidation
- Scroll restoration: Window or one active element, with an application-content readiness gate
See client data, error recovery, and scroll for the new
fetcher and scroll APIs, working examples, and their limits. These additions and
the .current change are included in 0.1.0, not 0.0.6.
<script>
import { Link, useNavigate } from '@hvniel/svelte-router';
const navigate = useNavigate();
</script>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<button onclick={() => navigate('/dashboard')}>
Go to Dashboard
</button>
</nav>This is the component registered by the data-router example above. The sample
assumes /api/users/:id returns a user object with a string name:
<!-- User.svelte -->
<script module lang="ts">
import type { LoaderFunctionArgs } from '@hvniel/svelte-router';
export async function loader({ params, request }: LoaderFunctionArgs) {
const response = await fetch(`/api/users/${params.id}`, {
signal: request.signal,
});
if (!response.ok) throw response;
const user: unknown = await response.json();
if (!user || typeof user !== 'object' || !('name' in user) || typeof user.name !== 'string') {
throw new Error('Expected a user with a string name');
}
return { user: { name: user.name } };
}
</script>
<script lang="ts">
import { useLoaderData, useParams } from '@hvniel/svelte-router';
const data = useLoaderData<typeof loader>();
const params = useParams<'id'>();
</script>
<h1>User: {data.current?.user.name ?? 'Loading...'}</h1>
<p>ID: {params.current.id}</p>Render this inside RouterProvider, with an action registered on the /login
data route. The form markup alone does not create the action or a server endpoint:
<script>
import { Form } from '@hvniel/svelte-router';
</script>
<Form method="post" action="/login">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</Form><!-- Layout.svelte -->
<script>
import { Outlet } from '@hvniel/svelte-router';
</script>
<div class="layout">
<header>My App</header>
<main>
<Outlet />
</main>
<footer>© 2024</footer>
</div>The main differences lie in how reactive values are handled and how route elements are defined, since Svelte uses a different component model than React:
In React Router, you can use either Component or element props for route definitions. In this Svelte port:
Component: Works the same as React Router - pass a Svelte componentelement: Instead of JSX elements, this accepts Svelte snippets
<script>
import { Route } from '@hvniel/svelte-router';
import UserProfile from './UserProfile.svelte';
</script>
<!-- Alternative definitions inside Routes; choose one. -->
<!-- Using Component (same as React Router) -->
<Route path="/user/:id" Component={UserProfile} />
<!-- Using element with snippets (Svelte-specific) -->
<Route path="/user/:id">
{#snippet element()}
<UserProfile />
{/snippet}
</Route>For data routing, put Component: UserProfile on the route object, as in the
entry example. Its alternative element property accepts a snippet reference,
not JSX or JavaScript written directly into component markup.
In both routing modes, value hooks return stable readonly .current handles.
Initialize them once; reads stay live without remounting the component:
<script>
import { useLocation, useParams } from '@hvniel/svelte-router';
const location = useLocation();
const params = useParams();
// Use $effect for side effects
$effect(() => {
console.log('Location changed:', location.current.pathname);
});
</script>
<p>{location.current.pathname}: {params.current.id ?? 'no user selected'}</p>Version 0.1.0 introduces a breaking change from 0.0.6. Replace
$derived(useLocation()) with useLocation() and read location.current.
Use getters for changing inputs: useHref(() => to, () => ({ relative })).
$derived remains useful for calculations from the current value. See the
reactivity guide for the full migration contract.
Declarative mode supplies browser, hash, and memory wrappers (fragments shown
below). Data mode uses the corresponding create*Router functions and
RouterProvider instead:
<script>
import { BrowserRouter, HashRouter, MemoryRouter } from '@hvniel/svelte-router';
</script>
<!-- Browser History (clean URLs) -->
<BrowserRouter>
<!-- routes -->
</BrowserRouter>
<!-- Hash History (hash-based URLs) -->
<HashRouter>
<!-- routes -->
</HashRouter>
<!-- Memory History (for testing/SSR) -->
<MemoryRouter initialEntries={["/"]}>
<!-- routes -->
</MemoryRouter>The library shares these core routing concepts with React Router:
- Routers: BrowserRouter, HashRouter, MemoryRouter, StaticRouter
- Route Matching: Path patterns, dynamic segments, nested routes
- Navigation: Link components, programmatic navigation
- Data Loading: Loaders, actions, and error boundaries
- Route Protection: Route guards and authentication flows
Use the original React Router documentation for conceptual background, and this package's parity plan for actual support and limitations. Client-side route protection is not server authorization.
| Component | Description |
|---|---|
RouterProvider |
Provides router context for data routing mode |
Outlet |
Renders child route components |
ScrollRestoration |
Restores window/element scroll in a data-router layout |
| Component | Description |
|---|---|
BrowserRouter |
Router using browser history API |
HashRouter |
Router using URL hash for navigation |
MemoryRouter |
Router storing history in memory |
StaticRouter |
Router for server-side rendering |
Routes |
Container for route definitions |
Route |
Individual route definition |
| Component | Description |
|---|---|
Link |
Navigation link component |
NavLink |
Link with active/pending state and snippet content |
Form |
Enhanced form with routing integration |
| Function | Description |
|---|---|
createBrowserRouter(routes, options?) |
Creates a data router with browser history |
createHashRouter(routes, options?) |
Creates a data router with hash history |
createMemoryRouter(routes, options?) |
Creates a data router with memory history |
| Hook | Description |
|---|---|
useNavigate() |
Returns a function for programmatic navigation |
useLocation() |
Read the location through .current |
useParams() |
Read current route params through .current |
useSearchParams() |
Returns [paramsHandle, setParams]; read paramsHandle.current |
useHref(to) |
Returns a live href handle; accepts a value or getter |
| Hook | Description |
|---|---|
useLoaderData<T>() |
Returns data from the route loader |
useActionData<T>() |
Returns data from the route action |
useRouteError() |
Returns the current route error |
useNavigation() |
Returns navigation state information |
useSubmit() |
Returns a function to submit forms programmatically |
useRevalidator() |
Returns a revalidate() command and live readonly state |
useFetcher<T>() |
Stable .current, key, load and submit controller |
useFetchers() |
Readonly handle listing active keyed operations |
useScrollRestoration(options?) |
Mount-owned scroll lifecycle; no return value |
Value readers such as useNavigation and useLoaderData return readonly .current handles. Loader and
action data may be undefined before it exists. useSubmit remains a command.
| Interface | Description |
|---|---|
LoaderFunction |
Type for route loader functions |
ActionFunction |
Type for route action functions |
RouteObject, DataRouteObject, NavigateOptions, SerializeFrom,
FetcherController, and scroll option/prop types are public. SerializeFrom
describes client results, not generated framework types or server serialization.
ErrorBoundary is a route property accepting a Svelte component, not an exported interface.
Start with AGENTS.md, then the relevant guide:
- Architecture and ownership
- Svelte reactivity and API design
- Development, compiler probes, and verification
These guides define modern Svelte conventions and how to correct outdated
assumptions. The parity plan tracks capability evidence;
the historical todos.md checklist is not a support contract.
MIT © Haniel Ubogu