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' | Full API. Any value import registers the <ui-router>/<ui-view> custom elements as a side effect. |
import { ... } from 'lit-ui-router/pure' | The same full 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 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>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 today; server rendering also needs a way to hand the directives a router, which is still open (#564):
<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 |
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
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