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

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