Skip to content

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

bash
npm install lit-ui-router
# or
pnpm add lit-ui-router

Entry Points

ImportEffect
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

ts
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();
html
<!-- 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 states
  • uiSrefActive - Adds CSS classes when linked state is active, and sets aria-current on active links
  • srefHref - uiSref bound in the href attribute
  • srefActiveClass - uiSrefActive's classes, bound in the class attribute
  • srefAriaCurrent - uiSrefActive's aria-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):

html
<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"> -->
  • srefHref takes uiSref's arguments and does everything uiSref does — the click navigates, and an enclosing uiSrefActive still tracks it. There is no assignHref: the attribute is the binding. Use one form or the other on an element, not both.
  • srefActiveClass follows lit's classMap: bind it in class, alone or beside static classes, and it toggles only the classes it names. It cannot share the attribute with classMap itself, so pass those classes as classes: { 'nav-link': true, disabled: locked }. Leave state out on a wrapper to watch the srefHref links inside it, as uiSrefActive does.
  • srefAriaCurrent is the one piece a class binding 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 of uiSrefActive's link detection or takeover rules. value picks 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:

ts
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.

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:

html
<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-current on 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 — leave aria-current alone entirely; the directive will neither set nor remove it. This is the opt-out to reach for when the application manages aria-current itself: 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. active applies 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 activeClasses and exactClasses do. Both class sets land in class on an exactly-active link, because an exact match is also an active one. aria-current is a single attribute with a single value, so the two are branches of one decision: an exactly-active element takes exact and never falls through to active. Each key keeps its own default when omitted, so { active: 'location' } alone still defaults exact to '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)

ts
{ name: 'home', url: '/', component: () => html`<h1>Home</h1>` }

Template with Route Parameters

ts
{
  name: 'user',
  url: '/user/:id',
  component: (props) => html`<h1>User ${props?.transition?.params().id}</h1>`
}

Template with Resolved Data

ts
{
  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)

ts
{ name: 'dashboard', url: '/dashboard', component: DashboardElement }
StyleBest For
() => html`...`Simple static views
(props) => html`...`Views needing params or resolves
MyElementComplex views with lifecycle, state, or styles

Lifecycle Hooks

Components can implement these interfaces to respond to routing events:

Injected Props

Routed components receive UIViewInjectedProps with:

  • router - The UIRouter instance
  • transition - The current transition
  • resolves - Resolved data from state declarations

Location Plugins

Import from @uirouter/core:

ts
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:

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