This document describes the public surface exported from @elelcode/lit-router.
Import both the class API and the custom element registration:
import { Router } from "@elelcode/lit-router";
import "@elelcode/lit-router";import {
DEFAULT_CHILD_SLOT,
type NavigationDirection,
type ReadonlyRouteDefinition,
type RouteComponentFactory,
type RouteContext,
type RouteContextReceiver,
type RouteDefinition,
type RouteErrorDetail,
type RouteGuard,
type RouteGuardResult,
type RouteInsertOptions,
type RouteLeaveGuard,
type RouteLoadContext,
type RouteLoadingDetail,
type RouteLocation,
type RouteMeta,
type RouteNotFoundDetail,
type RouteParamsInput,
type RouteParamValue,
type RouteQueryInit,
type RouteQueryValue,
Router,
type RouterChangeDetail,
type RouterChangeDetailJson,
type RouteResolveOptions,
type RouterEvent,
type RouterEventListener,
type RouterEventMap,
type RouterLinkAttributes,
type RouterLinkOptions,
type RouterMode,
type RouterSlotDetail,
type RouterSlotDetailJson,
RouterView,
type RouteSelector,
type RouteTreeChangeDetail,
} from "@elelcode/lit-router";Creates a router instance and starts it immediately unless
options.autoStart === false.
const router = new Router({
basePath: "/app",
routes: [
{ path: "", name: "home", component: "home-page" },
{ path: "settings", name: "settings", component: "settings-page" },
],
});Constructor options:
routes?: RouteDefinition[]basePath?: stringmode?: "history" | "hash"beforeRoute?: RouteGuardautoStart?: boolean
Notes:
basePathdefaults to"/".modedefaults to"history". In"hash"mode, route paths live after#(for example/#/app/settings) so static hosts do not need rewrite rules.- The router requires the Navigation API for same-document navigations, including hash-mode route changes.
- Only one started
Routerinstance is supported per document. - Use
configure()to update router options after construction.
The latest committed route state. Treat it as read-only application state.
Gets the current frozen read-only route tree snapshot.
Assigning router.routes = [...] replaces the full tree and triggers a route
refresh.
Returns a mutable cloned snapshot of the current route tree.
Current base path. Prefer configure({ basePath }) when changing it at runtime
so the router refreshes immediately.
In hash mode, basePath remains part of the application route path, but that
path is read from the hash payload. For example, basePath: "/app" matches
/#/app/settings instead of /app/settings.
Current routing mode. "history" uses the browser pathname. "hash" uses
#/path?query#fragment, so the same basePath and route tree can move between
history URLs such as /app/settings and hash URLs such as /#/app/settings. If
the initial document URL has no route hash, hash mode treats it as the
configured base route without rewriting the first URL.
Common URL shapes:
| Configuration | History URL | Hash URL |
|---|---|---|
basePath: "/" root |
/ |
/# |
basePath: "/" route |
/settings |
/#/settings |
basePath: "/app" root |
/app |
/#/app |
basePath: "/app" route |
/app/settings |
/#/app/settings |
WebView entry + basePath: "/app" |
n/a | /index.html#/app/settings |
Current global enter guard. Prefer configure({ beforeRoute }) when changing it
at runtime.
The latest route error, if the current state is an error state.
The latest not-found detail, if the current state is a not-found state.
Starts listening to browser navigation and resolves the current URL.
Throws when:
URLPatternis not available- another router is already started in the same document
Stops browser event handling and invalidates in-flight navigation work.
Updates one or more router options and refreshes the current URL once.
router.configure({
basePath: "/workspace",
beforeRoute: authGuard,
routes,
});Supported fields:
routesbasePathmodebeforeRoute
Replaces the entire route tree.
Use this when you really want a full tree swap. If you are enabling or disabling
features incrementally, prefer insertRoutes() and removeRoute().
Inserts one or more route definitions at runtime.
router.insertRoutes([
{ path: "reports", id: "reports-plugin", component: "reports-page" },
]);
router.insertRoutes([
{ path: "audit", id: "settings-audit", component: "audit-page" },
], {
parentId: "settings-shell",
index: 1,
});RouteInsertOptions:
parentId?: stringparentName?: stringindex?: number
Rules:
- Provide either
parentIdorparentName, not both. - Duplicate route
idand duplicate routenameare rejected. - Inserted child routes still require the parent component to expose the child slot.
- The router immediately re-matches the current URL after insertion.
Removes a runtime route branch by name or by selector object.
router.removeRoute("reports");
router.removeRoute({ id: "reports-plugin" });selector may be:
string: treated as a routenameRouteSelector:{ id?: string; name?: string }
Returns true when a route was removed, otherwise false.
Groups multiple tree updates into a single refresh.
router.batchRouteUpdates(() => {
router.insertRoutes([{
path: "alpha",
name: "alpha",
component: "alpha-page",
}]);
router.insertRoutes([{ path: "beta", name: "beta", component: "beta-page" }]);
});Rules:
- The callback must stay synchronous.
- If the callback throws, the router rolls the batch back.
- A successful batch emits one
route-tree-changeevent withreason: "batch".
Pushes a new navigation entry.
Accepted inputs:
stringURLRouteLocation
Navigates with replace semantics.
Accepted inputs are the same as push().
Resolves a string or URL without navigating. Returns null when the URL is
outside basePath or no route branch matches. Hash mode accepts both route URLs
such as /app/settings/profile and browser URLs such as
/#/app/settings/profile.
const match = router.resolveUrl("/settings/profile");Builds a named-route URL and resolves it without navigating.
const match = router.resolveNamed("message", {
params: { id: "42" },
query: { tab: "activity" },
});Builds an application-relative href from a named route.
const href = router.link({
name: "message",
params: { id: "42" },
query: { tab: "activity" },
hash: "summary",
});Notes:
- Requires a route
name. - In hash mode, returns an href with the route encoded after
#, for example/#/app/messages/42?tab=activity#summary. - A root route in hash mode emits
#whenbasePathis"/", and#/appwhenbasePathis"/app". - Supports literal segments,
:param,:param(<pattern>),*, and optional?tokens such as:lang?/docs. - Rejects
+and*parameter modifiers during reverse routing.
Builds attributes for a named-route anchor. Use this when JavaScript creates links that need replace semantics.
const attrs = router.linkAttributes(
{ name: "message", params: { id: "42" } },
{ replace: true },
);
// { href: "/messages/42", "data-router-replace": "" }In hash mode, plain fragments such as #section are left to the browser.
Route-looking hashes outside basePath, such as /#/outside when
basePath: "/app", are also not intercepted.
Listen on the Router instance:
router.addEventListener("route-change", (event) => {
console.log(event.detail.localPathname);
});Router.addEventListener() is typed for the built-in router event names, so
known events expose the corresponding CustomEvent.detail type without a local
cast.
The exported helper types are:
interface RouterEventMap {
"route-change": RouterChangeDetail;
"route-error": RouteErrorDetail;
"route-loading-start": RouteLoadingDetail;
"route-loading-end": RouteLoadingDetail;
"route-not-found": RouteNotFoundDetail;
"route-tree-change": RouteTreeChangeDetail;
}
type RouterEvent<K extends keyof RouterEventMap> = CustomEvent<
RouterEventMap[K]
>;
type RouterEventListener<K extends keyof RouterEventMap> = (
this: Router,
event: RouterEvent<K>,
) => void;Detail type: RouterChangeDetail
Fires after the router commits URL/current state.
Detail type: RouteTreeChangeDetail
Fires after runtime route-tree changes.
reason is one of:
"replace""insert""remove""batch"
routeCount reports the total number of route definitions. routes is the same
frozen read-only snapshot exposed by router.routes.
Detail type: RouteLoadingDetail
Fires before a branch starts lazy loading.
Detail type: RouteLoadingDetail
Fires after a branch finishes lazy loading, even when the load fails.
Detail type: RouteErrorDetail
Fires when navigation fails during:
- guard evaluation
- lazy loading
- view commit
- other internal failures reported as
"unknown"
Detail type: RouteNotFoundDetail
Fires when the current URL is inside basePath but no route matches.
<router-view> is the rendering outlet for a Router.
<router-view></router-view>The element is registered by importing the package side effect:
import "@elelcode/lit-router";Attaches the router instance to this outlet.
const outlet = document.querySelector("router-view")!;
outlet.router = router;When a router is attached, the outlet starts it if needed.
Configures the slot name used for nested child routes.
Default: DEFAULT_CHILD_SLOT, which is "route-child".
If a route has children, the parent component must expose a matching slot:
<slot name="route-child"></slot>Disables View Transitions for this outlet.
The latest detail committed into this outlet.
Imperative passthrough to router.push(url).
Imperative passthrough to router.replace(url).
Listen on the outlet when you care about DOM commit completion instead of router state only:
outlet.addEventListener("route-change", (event) => {
console.log("DOM committed", event.detail.pathname);
});Detail type: RouterChangeDetail
Fires after the outlet has:
- mounted the branch
- restored scroll
- moved focus
This is later than Router's route-change.
Detail type: RouteErrorDetail
Fires when commit fails inside the outlet, or when the attached router emits a route error.
Detail type: RouteNotFoundDetail
Fires when the attached router enters a not-found state.
Override the built-in fallback UI with light DOM slots:
<router-view>
<not-found-page slot="404"></not-found-page>
<route-error-page slot="error"></route-error-page>
</router-view>Available slots:
slot="404"slot="error"
The outlet host reflects navigation direction:
data-transition-direction="forward"data-transition-direction="backward"data-transition-direction="none"
Use this to write direction-aware View Transition CSS.
After a successful route commit:
- the outlet first tries to focus
[data-route-focus] - otherwise it focuses the viewport itself
- scroll restoration is tracked per history entry
- hash scrolling uses
document.getElementById()only
If an anchor target lives inside a shadow tree, expose the same id on the host
element so lookup stays O(1).
interface RouteDefinition {
id?: string;
name?: string;
path: string;
slot?: string;
title?: string;
viewTransitionName?: string;
component?: string | RouteComponentFactory;
children?: RouteDefinition[];
guard?: RouteGuard;
beforeLeave?: RouteLeaveGuard;
load?: (context?: RouteLoadContext) => Promise<unknown>;
meta?: RouteMeta;
props?: Record<string, unknown>;
}Field notes:
id: stable tree-management identifier for runtime insertion/removalname: stable route name forpush({ name }),replace({ name }), andlink()path: route segment. Use""for index routes and"*"for catch-allslot: parent slot name used by this route. Defaults to"route-child"."404"and"error"are reserved for<router-view>fallbacks.title: document title template.:paramtokens are expanded on commitviewTransitionName: route-levelview-transition-nameused by<router-view>when this route is the leafcomponent: custom-element tag name or factory returning anHTMLElementchildren: nested route tree rendered through a named slotguard: enter guard for this routebeforeLeave: leave guard for this route when its branch segment unloadsload: first-entry async loader. Its resolved value is ignored; use it for code splitting or side effects only. Receives{ signal }for cancellation.meta: route metadata for guards, events, and route context. It is shallow cloned on input, shallow frozen in read-only snapshots, and never assigned to the rendered DOM element.props: extra properties assigned onto the rendered element with a shallowObject.assign
Matching semantics:
- routes are compiled with specificity sorting
- static segments win over params
- params win over catch-all
- declaration order does not decide specificity bugs
interface RouteLocation {
name: string;
params?: RouteParamsInput;
query?: RouteQueryInit;
hash?: string;
}interface RouteResolveOptions {
params?: RouteParamsInput;
query?: RouteQueryInit;
hash?: string;
}interface RouterLinkOptions {
replace?: boolean;
}
interface RouterLinkAttributes {
href: string;
"data-router-replace"?: "";
}Route guards and leave guards may return:
trueundefinedfalsestringURL{ to: string | URL; replace?: boolean }
Semantics:
trueorundefined: allow navigationfalse: block navigation- redirect values: redirect immediately
type RouteGuard = (
detail: RouterChangeDetail & {
from: RouterChangeDetail | null;
router: Router;
signal: AbortSignal;
},
) =>
| boolean
| void
| string
| URL
| { to: string | URL; replace?: boolean }
| Promise<
| boolean
| void
| string
| URL
| { to: string | URL; replace?: boolean }
>;Order:
- global
beforeRoute - matched branch
guardfunctions from parent to leaf - each guard receives a navigation
signalthat aborts when a newer navigation supersedes it
type RouteLeaveGuard = (
detail: RouterChangeDetail & {
to: RouterChangeDetail | null;
router: Router;
signal: AbortSignal;
},
) =>
| boolean
| void
| string
| URL
| { to: string | URL; replace?: boolean }
| Promise<
| boolean
| void
| string
| URL
| { to: string | URL; replace?: boolean }
>;Order:
- runs only for the suffix being unloaded
- evaluated from leaf back toward the shared parent
interface RouteLoadContext {
signal: AbortSignal;
}Use signal to cancel work started by load() when a newer navigation
supersedes it.
Route elements should expose one of these contracts:
@property({ attribute: false })
accessor routeContext: RouteContext | undefined;or
setRouteContext(context: RouteContext): voidRouteContext:
interface RouteContext {
detail: RouterChangeDetail;
params: Record<string, string>;
slot: string;
branch: RouteDefinition[];
}detail.query is a URLSearchParams. slot is the slot this component was
projected into, and branch is the branch for that slot.
interface RouterChangeDetail {
pathname: string;
localPathname: string;
basePath: string;
search: string;
query: URLSearchParams;
hash: string;
params: Record<string, string>;
branch: RouteDefinition[];
leaf?: RouteDefinition;
slotBranches?: Record<string, RouteDefinition[]>;
slotParams?: Record<string, Record<string, string>>;
slots?: Record<string, {
branch: RouteDefinition[];
leaf?: RouteDefinition;
params: Record<string, string>;
}>;
url: URL;
historyKey: string;
direction: "forward" | "backward" | "none";
toJSON?: () => RouterChangeDetailJson;
}Field notes:
pathname: full browser pathnamelocalPathname: pathname relative tobasePathbranch: matched route branch from root to leafleaf: matched leaf routeslotBranches: matched non-main slot branches, keyed by slot nameslotParams: params for each non-main slot branchslots: slot-aware detail map including the main"route-child"slottoJSON: serializesqueryas a plain object andurlas a string forJSON.stringify(detail)historyKey: internal key for per-entry direction and scroll trackingdirection: computed navigation direction
interface RouterSlotDetail {
branch: RouteDefinition[];
leaf?: RouteDefinition;
params: Record<string, string>;
}interface RouteLoadingDetail {
url: URL;
branch: RouteDefinition[];
loadingSlots?: string[];
pending: number;
direction: NavigationDirection;
}pending is the current in-flight load count across the router. loadingSlots
contains the slot names whose matched branches need loading for this navigation.
interface RouteErrorDetail {
url: URL;
error: unknown;
phase: "guard" | "load" | "commit" | "unknown";
direction: NavigationDirection;
}interface RouteNotFoundDetail {
url: URL;
basePath: string;
direction: NavigationDirection;
}interface RouteTreeChangeDetail {
reason: "replace" | "insert" | "remove" | "batch";
routeCount: number;
routes: readonly ReadonlyRouteDefinition[];
}- Modern browser only.
URLPatternis required. - The router is designed for same-document SPA navigation, not SSR.
- Same-origin anchor interception is limited to the configured
basePath. - Lazy loads and navigation commits are race-safe.
- History direction bookkeeping is bounded instead of growing forever.
load()is deduplicated per route while a pending import is in flight.