API Overview
This guide provides a quick overview of the lit-ui-router API. For detailed type signatures and complete documentation, see the API Reference. Companion packages have their own home — see Companion Packages.
Installation
npm install lit-ui-router
# or
pnpm add lit-ui-routerEntry Points
| Import | Effect |
|---|---|
import { ... } from 'lit-ui-router' | The router, the elements, the directives and the controllers. Any value import registers the <ui-router>/<ui-view> custom elements as a side effect. |
import { ... } from 'lit-ui-router/pure' | The same API — element classes included — with no registration and no HTMLElementTagNameMap globals. |
import 'lit-ui-router/register' | Registration only: defines <ui-router>/<ui-view> and carries their HTMLElementTagNameMap entries. |
import 'lit-ui-router/ui-view.register' | Single-element registration: defines just that element with its tag-map entry (ui-router.register ditto). |
import { ... } from 'lit-ui-router/context' | The only home of the context-request key, event and request helper, and of the tree-less router hand-off — nothing from lit — see Reactive Components. |
import type { ... } from 'lit-ui-router' | Types are erased at compile time — always free, from any entry. |
Quick Start
import {
UIRouterLit,
uiSref,
uiSrefActive,
LitStateDeclaration,
} from 'lit-ui-router';
import { hashLocationPlugin } from '@uirouter/core';
import { html } from 'lit';
// 1. Create router and add location plugin
const router = new UIRouterLit();
router.plugin(hashLocationPlugin);
// 2. Define states
const states: LitStateDeclaration[] = [
{ name: 'home', url: '/home', component: () => html`<h1>Home</h1>` },
{ name: 'users', url: '/users', component: UserListElement },
];
// 3. Register states and start
states.forEach((state) => router.stateRegistry.register(state));
router.urlService.rules.initial({ state: 'home' });
router.start();<!-- 4. Use in your app -->
<ui-router .uiRouter="${router}">
<nav>
<a ${uiSref('home')} ${uiSrefActive({ activeClasses: ['active'] })}>Home</a>
<a ${uiSref('users')} ${uiSrefActive({ activeClasses: ['active'] })}>Users</a>
</nav>
<ui-view></ui-view>
</ui-router>rules.initial({ state: 'home' }) lands on the state with no params; a landing state that reads the query needs the function form — see Unmatched URLs.
Core Concepts
Router
UIRouterLit is the main router class. It extends @uirouter/core's UIRouter with Lit-specific view handling.
Components
<ui-router>- Root component that provides router context to descendants<ui-view>- Viewport that renders the component for the current state
Directives
uiSref- Creates navigation links to statesuiSrefActive- Adds CSS classes when linked state is active, and setsaria-currenton active linkssrefHref-uiSrefbound in thehrefattributesrefActiveClass-uiSrefActive's classes, bound in theclassattributesrefAriaCurrent-uiSrefActive'saria-current, bound in the attribute
Attribute-part forms
uiSref and uiSrefActive are element parts: they sit on the element and write to it from the outside, so a reader that is not a live browser — a server renderer, an accessibility linter — sees an <a> with no href. The sref* directives do the same jobs from inside the attribute they affect, so the template says what the browser will show. A linter reads that, and so does a server render given a router — see Server Rendering:
<a href=${srefHref('users')}
class="nav-link ${srefActiveClass({ state: 'users', activeClasses: ['active'] })}"
aria-current=${srefAriaCurrent({ state: 'users' })}>Users</a>
<!-- while at `users`: <a href="/users" class="nav-link active" aria-current="page"> -->
<!-- while at `users.detail`: <a href="/users" class="nav-link active"> -->srefHreftakesuiSref's arguments and does everythinguiSrefdoes — the click navigates, and an enclosinguiSrefActivestill tracks it. There is noassignHref: the attribute is the binding. Use one form or the other on an element, not both.srefActiveClassfollows lit'sclassMap: bind it inclass, alone or beside static classes, and it toggles only the classes it names. It cannot share the attribute withclassMapitself, so pass those classes asclasses: { 'nav-link': true, disabled: locked }. Leavestateout on a wrapper to watch thesrefHreflinks inside it, asuiSrefActivedoes.srefAriaCurrentis the one piece aclassbinding cannot reach, so it is its own directive. Binding the attribute is the opt-in: it writes'page'while the exact state is active and removes the attribute otherwise, on any element, with none ofuiSrefActive's link detection or takeover rules.valuepicks another token or{ exact, active }for ancestors.
Composing with classMap
A class attribute holds one toggling directive, so srefActiveClass and classMap cannot share it. When a component wants both — the active flag next to its own bindings — SrefStatusController hands the status to the host instead of writing an attribute, and the template composes it freely:
private users = new SrefStatusController(this, { state: 'users' });
// class=${classMap({ 'nav-link': true, active: this.users.active, disabled: this.locked })}
// aria-current=${this.users.ariaCurrent()}See Reactive Components.
Accessible active links
uiSrefActive conveys active state to assistive technology as well as to CSS. On a link element (<a>, <area>, or anything with role="link") it sets aria-current="page" while the exact linked state is active, and removes the attribute when it is not — so the nav above needs no extra markup:
<a ${uiSref('users')} ${uiSrefActive({ activeClasses: ['active'] })}>Users</a>
<!-- while at `users`: <a href="/users" class="active" aria-current="page"> -->
<!-- while at `users.detail`: <a href="/users" class="active"> -->aria-current="page" is applied on exact match only, deliberately: an ancestor state being active means the link points at a section containing the current page, not at the current page itself, and a nav in which several ancestor links all claim aria-current="page" is worse for a screen reader user than one with none.
Three knobs, via ariaCurrentValue:
Another token —
'page'(default),'step','location','date','time', or'true'. Passing a value explicitly also opts non-link elements in, which is otherwise off;aria-currenton a wrapping<li>or<tr>is valid ARIA but rarely what an author means, so wrappers stay silent unless asked.html<li ${uiSrefActive({ activeClasses: ['active'] })}> <!-- classes on the wrapper, aria-current on the link --> <a ${uiSref('users')} ${uiSrefActive({})}>Users</a> </li> <!-- an explicit value opts a non-link element in; 'auto' keeps href off it --> <tr ${uiSref('.message', { messageId }, { assignHref: 'auto' })} ${uiSrefActive({ activeClasses: ['active'], ariaCurrentValue: 'true' })}></tr>false— leavearia-currentalone entirely; the directive will neither set nor remove it. This is the opt-out to reach for when the application managesaria-currentitself: a value written in the template survives untouched, in every routing state.html<a ${uiSref('home')} ${uiSrefActive({ activeClasses: ['active'], ariaCurrentValue: false })}>Home</a>{ exact, active }— mark ancestors too, which is otherwise off.activeapplies while a child state is active and this one is not the exact match;'location'is the ARIA token meant for exactly that.html<a ${uiSref('users')} ${uiSrefActive({ activeClasses: ['active'], ariaCurrentValue: { exact: 'page', active: 'location' }, })}>Users</a> <!-- while at `users`: aria-current="page" --> <!-- while at `users.detail`: aria-current="location" -->Note this pair does not combine the way
activeClassesandexactClassesdo. Both class sets land inclasson an exactly-active link, because an exact match is also an active one.aria-currentis a single attribute with a single value, so the two are branches of one decision: an exactly-active element takesexactand never falls through toactive. Each key keeps its own default when omitted, so{ active: 'location' }alone still defaultsexactto'page'on links — write{ exact: false, active: 'location' }to mark only ancestors.
The directive only removes an aria-current it wrote itself. A value authored in the template is therefore left alone — but only until the directive first writes one of its own, after which it owns the attribute and will clear it on the next inactive render. ariaCurrentValue: false is the way to keep a template-authored value for good, since the directive then never writes and never takes ownership.
State Declaration
LitStateDeclaration defines a state with its URL and component.
Component Styles
lit-ui-router supports multiple ways to define route components:
Inline Template Function (simplest)
{ name: 'home', url: '/', component: () => html`<h1>Home</h1>` }Template with Route Parameters
{
name: 'user',
url: '/user/:id',
component: (props) => html`<h1>User ${props?.transition?.params().id}</h1>`
}Template with Resolved Data
{
name: 'users',
url: '/users',
component: (props) => html`
<ul>${props?.resolves?.users?.map(u => html`<li>${u.name}</li>`)}</ul>
`,
resolve: [{ token: 'users', resolveFn: () => fetchUsers() }]
}LitElement Class (for complex components with lifecycle/state)
{ name: 'dashboard', url: '/dashboard', component: DashboardElement }| Style | Best For |
|---|---|
() => html`...` | Simple static views |
(props) => html`...` | Views needing params or resolves |
MyElement | Complex views with lifecycle, state, or styles |
Typing Resolves
LitStateDeclaration<T> takes the resolves type as a generic, defaulting to Record<string, any>. A view receives T as an argument, so the check runs in reverse: a view that requires a resolve does not fit the default, and TypeScript reports the error at component: rather than at the generic.
const GalleryView: RoutedLitTemplate<{ manifest: Manifest }> = (props) =>
html`<p>${props.resolves.manifest.sheets.length}</p>`;
const states: LitStateDeclaration[] = [
{ name: 'gallery', url: '/', component: GalleryView }, // TS2322 at component:
];Two shapes type-check:
- Thread the generic onto the declaration:
LitStateDeclaration<{ manifest: Manifest }>[]. A bareRoutedLitTemplateview still fits beside the typed one. - A table mixing resolve shapes threads one type alias that covers every view, such as
GalleryResolves & SheetResolves. Each view keeps its own required members. An alias with every member optional also fits, at the cost of?.at each use inside the views.
Lifecycle Hooks
Components can implement these interfaces to respond to routing events:
UiOnExit- Called before navigating away (can cancel navigation)UiOnParamsChanged- Called when route parameters change
Injected Props
Routed components receive UIViewInjectedProps with:
router- The UIRouter instancetransition- The current transitionresolves- Resolved data from state declarations
Location Plugins
Import from @uirouter/core:
import { hashLocationPlugin, pushStateLocationPlugin } from '@uirouter/core';
// Hash URLs: /#/home
router.plugin(hashLocationPlugin);
// HTML5 pushState: /home
router.plugin(pushStateLocationPlugin);See the @uirouter/core location plugins documentation:
- PushStateLocationService - HTML5 history API
- HashLocationService - Hash-based URLs
On the server, serverLocationPlugin from ui-router-server/location is the in-memory plugin whose hrefs are paths, so a prerendered link matches what the pushState client writes.
Server Rendering
lit-ui-router/context carries the router hand-off a server render needs and an element cannot supply, because on the server a directive has no element to seek a router from.
provideRouter(root, router)- answerscontext-requeston any event target,globalThis.litServerRootincludedwithRouterSync(router, run)- scopes a router to one synchronous callgetScopedRouter()- reads the router that call scoped
srefHref, srefActiveClass, srefAriaCurrent and lit-ui-router-effect's RouterRefController read the withRouterSync slot and nothing else, so a nav rendered inside it carries real hrefs and active markup; provideRouter serves elements that ask by protocol, not attribute parts.
See Server-Side Routing.
Companion Packages
lit-ui-router is the core package; optional companion packages layer on extra integrations — MobX bindings, the Navigation API location plugin, and server-side routing verdicts with ui-router-server (in development), each independently versioned with its own guide and API reference. See Companion Packages.
Further Reading
- Tutorial - Step-by-step guide
- API Reference - Complete type documentation
- @uirouter/core docs - Core router documentation