---
name: iyulab-router
description: Client-side SPA router for Lit and React with URLPattern matching, nested routes, route guards, metadata merging, fallback handling, and route events. Use when working with @iyulab/router to define routes, add guards, handle navigation, or integrate <u-outlet>/<u-link>.
license: MIT
compatibility: Browser environments only (requires URLPattern and History API)
metadata:
  author: iyulab
---

# @iyulab/router

Client-side router supporting Lit and React renders, nested routes, route guards, and URLPattern-based matching.

## Install

```bash
npm install @iyulab/router
```

One copy per app: `npm ls @iyulab/router` must list a single version. Packages that take the router as a peer
(`@iyulab/modern-app`) share the app's copy. A broad peer range (`"*"`) in a workspace package can keep an older
copy installed with no install error — delete the older lockfile and `node_modules` entries and reinstall. A second
copy that loads throws «`<u-link>` is already defined by a different class … another copy of @iyulab/router».

## Core API

| Export | Purpose |
|---|---|
| `Router` | Main router class |
| `RouteConfig` | Route definition type |
| `RouterConfig` | Constructor config type |
| `RouteContext` | Passed to every `render()` call |
| `FallbackRouteConfig` | Error/404 fallback definition |
| `<u-outlet>` | Renders the matched route output (a block box — see `references/components.md`) |
| `<u-link>` | Client-side navigation anchor |
| `UOutlet`, `ULink` | React wrappers (from `@iyulab/router/react`) |

## Router Setup

```ts
import { Router } from '@iyulab/router';
import { html } from 'lit';

const router = new Router({
  root: document.body,       // required — mount element containing <u-outlet>
  basepath: '/',             // optional
  enter: (ctx) => {
    if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
    return true;
  },
  routes: [
    { index: true, render: () => html`<home-page></home-page>` },
    { path: '/user/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
    {
      path: '/admin',
      metadata: { role: 'admin' },
      enter: (ctx) => ctx.metadata.role === 'admin' || '/forbidden',
      render: () => html`<admin-page></admin-page>`,
    },
  ],
  fallback: {
    render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`
  }
});
```

## RouteConfig Fields

| Field | Type | Description |
|---|---|---|
| `path` | `string \| URLPattern` | URLPattern path; omit when `index: true` |
| `index` | `true` | Marks route as index of its parent path |
| `render` | `(ctx) => unknown` | Returns Lit `TemplateResult`, React element, or `HTMLElement` |
| `enter` | `(ctx) => string \| boolean \| Promise<string \| boolean>` | Guard before route render (`false` cancel, `string` redirect) |
| `leave` | `({ from, to }) => boolean \| Promise<boolean>` | Guard before the current screen is replaced (`false` stays — also for Back/Forward). See `references/guards-and-metadata.md` |
| `children` | `RouteConfig[]` | Nested routes; parent must render `<u-outlet>` |
| `title` | `string` | Sets `document.title` on match |
| `metadata` | `Record<string, unknown>` | Arbitrary metadata (auth, layout, analytics) |
| `key` | `(ctx) => string` | When to remount: content is kept and re-rendered in place while the key is unchanged (Lit: same part, React: same root, `HTMLElement`: instance kept). Default `ctx => ctx.href` for leaf routes, a constant for routes with `children`. `key: ctx => ctx.pathname` keeps a page across query-string changes |

## RouteContext Fields

```ts
ctx.params     // URLPattern captured params
ctx.pathname   // path without query/hash
ctx.path       // full path including query + hash
ctx.query      // URLSearchParams
ctx.metadata   // merged metadata from matched route chain
ctx.progress   // (value: number) => void — report 0–100 loading progress
```

## Nested Routes

Parent must include `<u-outlet>` in its render output:

```ts
{
  path: '/dashboard',
  render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
  children: [
    { index: true, render: () => html`<dashboard-home></dashboard-home>` },
    { path: 'settings', render: () => html`<dashboard-settings></dashboard-settings>` }
  ]
}
```

## Navigation

```ts
router.go('/path');                  // programmatic navigation
router.go('relative-path');         // relative to basepath
router.destroy();                   // remove event listeners

// From Lit template
html`<u-link href="/about">About</u-link>`

// From React
import { ULink } from '@iyulab/router/react';
<ULink href="/about">About</ULink>
```

`<u-link>` supports `href`, `target`, and `rel`.

```html
<u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
```

## Route Events (window)

| Event | Fired when |
|---|---|
| `route-begin` | Navigation starts |
| `route-progress` | Async progress update (0–100) |
| `route-done` | Navigation completes |
| `route-error` | Routing error occurs |

## Error Types (fallback ctx.error)

| Code | Class |
|---|---|
| `NOT_FOUND` | `NotFoundError` |
| `CONTENT_LOAD_ERROR` | `ContentLoadError` |
| `CONTENT_RENDER_ERROR` | `ContentRenderError` |

References:
- [references/routing-basics.md](references/routing-basics.md)
- [references/url-pattern.md](references/url-pattern.md)
- [references/guards-and-metadata.md](references/guards-and-metadata.md)
- [references/components.md](references/components.md)
- [references/events-and-errors.md](references/events-and-errors.md)
