Routes and navigation
Register routes, mount the router, navigate programmatically, and read the current route from any island.
router() creates a fresh router instance and resets the route registry. Always call it fresh — never share instances across server requests.
router(options)
| Option | Type | Default | Description |
|---|---|---|---|
mode |
"spa" | "static" |
"spa" |
"static" disables client navigation — hydrate with hydrateStatic() |
interceptLinks |
boolean |
true |
Intercept internal <a> clicks for SPA navigation |
notFound |
Island |
— | Custom 404 island rendered when no route matches (see RouterView) |
allowExternalRedirects |
boolean |
false |
Allow loader redirect() to cross-origin URLs; blocked targets become a 500 error |
loaderTimeout |
number |
— | Max loader runtime in ms — see Loaders |
viewTransitions |
boolean |
false |
Wrap client view swaps in document.startViewTransition() when supported |
.route(pattern, island, loader?)
Registers a route. Patterns support :param segments and a trailing /**:name catch-all; static segments take priority over params, which take priority over the catch-all — regardless of registration order.
| Pattern | Matches | routeParams() |
|---|---|---|
/ |
/ |
{} |
/user/:id |
/user/42 |
{ id: "42" } |
/:org/:repo |
/ilha/router |
{ org: "ilha", repo: "router" } |
/docs/**:slug |
/docs/guide/intro |
{ slug: "guide/intro" } |
/** |
anything | {} |
Static segments always take priority over :param segments.
.mount(target, options?) — browser only
Mounts the router into a DOM element or CSS selector. Sets up popstate listening and intercepts internal <a> clicks automatically. Returns an unmount function.
| Option | Type | Default | Description |
|---|---|---|---|
hydrate |
boolean |
false |
Preserve SSR DOM, don’t wipe on first mount |
registry |
Record<string, Island> |
undefined |
Island registry for interactive hydration on navigation |
While mounted, the router takes over scroll restoration (history.scrollRestoration = "manual"): pushes scroll to the top or to the #hash target, and back/forward restores the position saved for that history entry — in both directions. Opt out per navigation with navigate(to, { scroll: false }). Positions live in memory only; after a full reload the browser’s own restoration takes over.
navigate(to, options?)
Programmatically navigate to a path. Updates the URL, history stack, and all reactive signals. Duplicate navigations are no-ops.
import { navigate } from "@ilha/router";
navigate("/about");
navigate("/about", { replace: true }); // replaces instead of pushing
navigate("/about", { scroll: false }); // keep the current scroll position
Scroll resets to the top (or the URL’s hash target) when the navigation lands on a different island. A same-island navigation — a param or query-string change like ?page=2 — keeps the current scroll position, since it’s a data change, not a page change; pass an explicit hash to scroll to a target anyway.
prefetch(pathWithSearch)
Prefetches loader data for a path in the background. The result is cached and consumed on the next navigation, making the transition feel instant.
import { prefetch } from "@ilha/router";
prefetch("/user/42");
prefetch("/dashboard?tab=overview");
RouterLink calls this automatically on mouseenter for links with the data-prefetch attribute.
Prefetched entries are single-use and expire after ~30 seconds. If the navigation that would consume a prefetch is superseded mid-flight, the prefetched result is discarded rather than applied to the wrong view.
navigating()
Reactive — true while a client navigation (loader fetch + view swap) is in flight. Read it inside any island render to drive a progress bar or spinner. Also available as useRoute().navigating.
import { navigating } from "@ilha/router";
import { ilha } from "ilha";
const Spinner = ilha(() =>
navigating() ? <div class="bar" /> : null,
);
invalidate()
Re-runs the current route’s loader and re-renders the view with fresh data — call it after a mutation. Resolves when the view has updated. No-op on the server or when no router is mounted.
import { invalidate } from "@ilha/router";
await api.deleteItem(id);
await invalidate(); // current page refetches and re-renders
Hash mode
By default the router uses the HTML5 History API (location.pathname). That needs a server or host that serves your SPA shell for every URL. When that is not possible — file://, Electron, or hosts without SPA fallback — use hash mode so the route lives in location.hash:
import { setHistoryMode, router } from "@ilha/router";
setHistoryMode("hash"); // once, before .mount() / .hydrate() / prime()
router()
.route("/", HomePage)
.route("/about", AboutPage)
.mount("#app");
URLs look like index.html#/about or index.html#/user/42?tab=1. navigate("/about") still takes a logical path (no # prefix). RouterLink emits href="#/…" in hash mode.
SSR + hydration is not supported in hash mode — the hash is never sent to the server. Use plain .mount("#app") without { hydrate: true }. Loaders can still run on the client via /__ilha/loader or runLoader().
History mode is process-global (navigate, RouterLink, and prefetch share it). Set it once at app entry.
Route context
These signals reflect the current route and are safe to read inside any island on both server and client.
useRoute()
Returns reactive accessors for the current route state as a convenience object:
import { ilha } from "ilha";
import { useRoute } from "@ilha/router";
const MyPage = ilha(() => {
const { path, params, search, hash } = useRoute();
return <p>User: {params().id}</p>;
});
routePath · routeParams · routeSearch · routeHash
The underlying context signals for direct access outside of islands:
import { routePath, routeParams } from "@ilha/router";
routePath(); // → "/user/42"
routeParams(); // → { id: "42" }
isActive(pattern, options?)
Returns true if the current path matches a registered pattern:
import { isActive } from "@ilha/router";
isActive("/about"); // → true on /about
isActive("/user/:id"); // → true on /user/42 (the matched route's pattern)
isActive("/docs", { exact: false }); // → true on /docs and /docs/anything
The default (exact) form compares against the matched route’s registered pattern — an island registered under several patterns reports whichever pattern actually matched the current path. Pass { exact: false } for a prefix match on the current path instead (useful for highlighting a nav section across nested pages).
Built-in islands
RouterView
The outlet island rendered by .mount() and .render(). Wraps the active page island in <div data-router-view>.
When no route matches: with router({ notFound }), the 404 island renders inside <div data-router-view data-router-not-found> — server renders return it with HTTP status 404 (via renderResponse), and in the browser it is mounted with a full island lifecycle (events and effects work) in both SPA and hydrate modes. Without notFound, the outlet renders <div data-router-empty></div>.
Server render methods use the notFound of the router instance they were called on, so concurrent routers can’t clobber each other; the browser helpers follow the last-mounted router.
RouterLink
A declarative link island. Calls navigate() on click and prefetches loader data on mouseenter.
import { RouterLink } from "@ilha/router";
RouterLink.toString({ href: "/about", label: "About" });
// → '<a data-link data-prefetch href="/about">About</a>'
Opt a specific link out of prefetching with data-prefetch="false".
Transitions
Three building blocks, from zero-config to custom:
-
View transitions —
router({ viewTransitions: true })wraps every client view swap indocument.startViewTransition(). Animate with standard CSS::view-transition-old/newrules. -
Pending attribute —
<html data-router-pending>while any navigation is in flight. Style skeletons against it without touching JS:[data-router-pending] [data-router-view] { opacity: 0.4; pointer-events: none; } -
navigating()— reactive signal for JS-driven UI (progress bars, spinners); see Navigation helpers.
Route sorting
Routes are sorted automatically — no need to order files manually:
- Static paths (
/about) — highest priority - Parameterised paths (
/user/:id) - Wildcard paths (
/**:slug) — lowest priority
Within the same tier, longer segment counts and alphabetical order act as tiebreakers.