npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wcstack/router

v1.3.17

Published

Declarative SPA routing for Web Components with Navigation API support. Zero dependencies, buildless.

Readme

@wcstack/router

What if routing was just HTML tags?

Imagine a future where you define your app's navigation structure in markup — nested routes, layouts, typed parameters — all as native HTML elements. No router config objects, no JavaScript ceremony. Just tags that describe where things go.

That's what <wcs-router>, <wcs-route>, and friends explore. One CDN import, zero dependencies, pure HTML syntax.

Features

Basic Features

  • Declarative Routing: Simply list <wcs-route> tags within an HTML <template>. No JS configuration object required.
  • Nested Route Definitions: Intuitively express nested structures like /products/:id.
  • Parameter Support: Supports path parameters (:id).
  • Fallback (404): Handle undefined paths with <wcs-route fallback>.
  • Navigation API Based: Built on the modern standard Navigation API, offering high affinity with native browser behavior.
  • Zero Config / Buildless: Works directly in the browser without bundling.

Unique Features

  • Light DOM Layout System: Defines layout templates in normal DOM (Light DOM) without forcing Shadow DOM. Makes global CSS application and <slot> insertion easy.
  • Typed Parameters: Specify type constraints like :id(int). Automatically converts values to number type.
  • Mixed Layouts & Routes: Freely nest <wcs-layout> within the routing tree, managing layout switching per area purely through HTML structure.
  • Auto-Binding: Automatically injects URL parameters into components using data-bind attribute (supports props, states, attr, and direct property modes).
  • Declarative <head> Management: Declaratively switch title and meta tags for each page using <wcs-head>.

Usage

<wcs-router>
	<template>
		<!-- When path is "/" -->
		<wcs-route path="/">
			<!-- Apply the "main-layout" layout -->
			<wcs-layout layout="main-layout">
				<main-header slot="header"></main-header>
				<main-body>
					<!-- When path is "/" -->
					<wcs-route index>
						<wcs-head>
							<title>Main Page</title>
						</wcs-head>
						<main-dashboard></main-dashboard>
					</wcs-route>

					<!-- When path is "/products" (relative paths below top-level) -->
					<wcs-route path="products">
						<wcs-head>
							<title>Product Page</title>
						</wcs-head>
						<!-- When path is "/products" -->
						<wcs-route index>
							<product-list></product-list>
						</wcs-route>
						<!-- When path is "/products/:productId" -->
						<wcs-route path=":productId">
							<!-- productItem.props.productId = productId -->
							<product-item data-bind="props"></product-item>
						</wcs-route>
					</wcs-route>
				</main-body>
			</wcs-layout>
		</wcs-route>

		<!-- When path is "/admin" -->
		<wcs-route path="/admin">
			<!-- Apply the "admin-layout" layout -->
			<wcs-layout layout="admin-layout">
				<wcs-head>
					<title>Admin Page</title>
				</wcs-head>
				<admin-header slot="header"></admin-header>
				<admin-body></admin-body>
			</wcs-layout>
		</wcs-route>

		<!-- When no path matches -->
		<wcs-route fallback>
			<error-404></error-404>
		</wcs-route>
	</template>
</wcs-router>

<wcs-outlet>
	<!-- Build a DOM tree according to the route path and layout and render it here -->
</wcs-outlet>

<!-- "main-layout" layout -->
<template id="main-layout">
	<section>
		<h1> Main </h1>
		<slot name="header"></slot>
	</section>
	<section>
		<slot></slot>
	</section>
</template>

<!-- "admin-layout" layout -->
<template id="admin-layout">
	<section>
		<h1> Admin Main </h1>
		<slot name="header"></slot>
	</section>
	<section>
		<slot></slot>
	</section>
</template>
  • are custom components in your app.
  • The custom elements above must be defined separately (via an autoloader or manual registration).

Reference

Router (wcs-router)

Define routes and layout slots inside a child template tag. Only one can exist in a document. A direct child template tag is required. Outputs according to definitions to <wcs-outlet>.

| Attribute | Description | |------|------| | basename | When routing in a subfolder URL, specify the subfolder. Not required if you don’t run in a subfolder. |

Route (wcs-route)

Displays children when the route path matches. Match priority is static paths over parameters.

| Attribute | Description | |------|------| | path | For top-level routes, specify an absolute path starting with /. Otherwise, specify a relative path. For parameters, use :paramName. For catch-all, use *. Top-level routes cannot use relative paths. | | index | Inherits the upper path. | | fallback | Displayed when no route matches the path. | | fullpath | Path including parent routes (read-only). | | name | Identifier. | | guard | Enables guard handling. Specify the full path to navigate to on guard cancellation. |

| Property | Description | |------|------| | params | Matched parameters (strings). | | typedParams | Matched parameters (converted types). | | guardHandler | Sets the guard decision function. |

Guard decision function type: function (toPath: string, fromPath: string): boolean | Promise

Typed Parameters

By specifying types for path parameters, you can perform value validation and automatic conversion.

Syntax: :paramName(typeName)

<!-- Integer parameter -->
<wcs-route path="/users/:userId(int)">
  <user-detail></user-detail>
</wcs-route>

<!-- Complex parameters -->
<wcs-route path="/posts/:date(isoDate)/:slug(slug)">
  <post-detail></post-detail>
</wcs-route>

Built-in Types:

| Type Name | Description | Example | Converted Type | |------|------|------|------| | int | Integer | 123, -45 | number | | float | Floating point number | 3.14, -2.5 | number | | bool | Boolean | true, false, 0, 1 | boolean | | uuid | UUID v1-5 | 550e8400-e29b-41d4-a716-446655440000 | string | | slug | Slug (lowercase alphanumeric and hyphens) | my-post-title | string | | isoDate | ISO 8601 Date | 2024-01-23 | Date | | any | Any string (default) | Any | string |

Retrieving Values:

// Get from the route element
const route = document.querySelector('wcs-route[path="/users/:userId(int)"]');

// Get as string
console.log(route.params.userId);       // "123"

// Get as typed value
console.log(route.typedParams.userId);  // 123 (number)

Behavior:

  • If the value does not match the type, the route will not match (it does not result in an error).
  • If no type is specified, it is treated as any (same as previous behavior).
  • Specifying an unknown type name also falls back to any.

Layout (wcs-layout)

Loads a template, inserts children into <slot>, and writes to <wcs-layout-outlet>. Light DOM supported. External file supported.

| Attribute | Description | |------|------| | layout | The id attribute of the template tag used as the template. | | src | URL of an external template file. | | name | Identifier passed to wcs-layout-outlet. | | enable-shadow-root | Use Shadow DOM in <wcs-layout-outlet>. | | disable-shadow-root | Use Light DOM in <wcs-layout-outlet>. |

Outlet (wcs-outlet)

Displays a DOM tree according to the routing and layout settings. Define it in HTML, or if missing it is created by <wcs-router>.

LayoutOutlet (wcs-layout-outlet)

Displays a DOM tree into <wcs-outlet> according to the layout (<wcs-layout>) settings. Inherits the name attribute from <wcs-layout>. Use the name attribute to identify styling targets.

| Attribute | Description | |------|------| | name | The name attribute of <wcs-layout>. Use it to identify styling targets. |

Light DOM Limitations

When utilizing disable-shadow-root (Light DOM), slot replacement targets only direct children of <wcs-layout>. Elements with slot attributes inside <wcs-route> will not be placed in the slot.

<!-- NG: <div slot="header"> is not a direct child of wcs-layout, so it doesn't go into the slot -->
<wcs-layout layout="main" disable-shadow-root>
  <wcs-route path="/page">
    <div slot="header">Header Content</div>
  </wcs-route>
</wcs-layout>

<!-- OK: Make the element with slot attribute a direct child of wcs-layout -->
<wcs-layout layout="main" disable-shadow-root>
  <div slot="header">Header Content</div>
  <wcs-route path="/page">
    <!-- Page content -->
  </wcs-route>
</wcs-layout>

In the case of enable-shadow-root (Shadow DOM), this limitation does not apply because the native <slot> function is used.

Head (wcs-head)

Manages document <head> elements per route. Uses a stack-based system where the most recently connected Head is prioritized.

<wcs-route path="/about">
  <wcs-head>
    <title>About Us</title>
    <meta name="description" content="About our company">
  </wcs-head>
  <about-page></about-page>
</wcs-route>

Supported elements: <title>, <meta>, <link>, <base>, <script>, <style>

Behavior:

  • Captures the initial <head> state on first connection
  • When multiple <wcs-head> elements are active, the last connected one takes priority
  • When all <wcs-head> elements disconnect, the initial state is restored
  • Elements are identified by key (e.g., <meta> by name/property/http-equiv, <link> by rel/href)

Link (wcs-link)

Link. Converted to an <a>, and the route path in the to attribute is converted to a URL. When the link path matches the current URL, the active CSS class is automatically added to the generated <a> element.

| Attribute | Description | |------|------| | to | Destination path or URL. Paths starting with / are treated as internal paths (basename is prepended). Other values are treated as external URLs. |

Active state: The generated <a> receives the active class when its path matches the current location. Tracking is updated on navigation events (currententrychange, wcs:navigate, popstate).

/* Style active links */
a.active { font-weight: bold; color: blue; }

Auto-Binding (data-bind)

Elements with the data-bind attribute automatically receive matched route parameters. Four binding modes are available:

| data-bind value | Target | Description | |------|------|------| | "props" | element.props | Merges params into the props property | | "states" | element.states | Merges params into the states property | | "attr" | HTML attributes | Sets params as HTML attributes via setAttribute() | | "" (empty) | Direct properties | Sets params directly on the element (e.g., element.id = value) |

<wcs-route path="/users/:userId(int)">
  <!-- element.props = { userId: 123 } -->
  <user-detail data-bind="props"></user-detail>

  <!-- element.setAttribute("userId", 123) -->
  <div data-bind="attr"></div>
</wcs-route>

Parameters are assigned before connectedCallback fires. For custom elements that are not yet defined, assignment is deferred until customElements.whenDefined() resolves.

Configuration

Initialize the router with optional configuration via bootstrapRouter():

import { bootstrapRouter } from '@wcstack/router';

bootstrapRouter({
  // Custom tag names (all optional)
  tagNames: {
    router: 'wcs-router',       // default
    route: 'wcs-route',         // default
    outlet: 'wcs-outlet',       // default
    layout: 'wcs-layout',       // default
    layoutOutlet: 'wcs-layout-outlet', // default
    link: 'wcs-link',           // default
    head: 'wcs-head'            // default
  },
  // Use Shadow DOM for outlets (default: false)
  enableShadowRoot: false,
  // File extensions stripped from basename (default: [".html"])
  basenameFileExtensions: [".html"]
});

Path Specification (Router / Route / Link)

Terminology

  • URL Pathname: location.pathname (e.g. /app/products/42)
  • basename: The app mount path (e.g. /app)
  • internalPath: The routing path inside the app after removing basename (e.g. /products/42)

1) basename specification

1.1 basename resolution order

  1. The basename attribute on <wcs-router basename="/app">
  2. If <base href="/app/"> exists, derive from new URL(document.baseURI).pathname
  3. If neither exists, use empty string "" (assumes running at root)

1.2 basename normalization (important)

basename is always normalized as follows:

  • Add leading / (except empty string)
  • Collapse multiple slashes into one
  • Remove trailing / (except / itself, which is treated as empty)
  • Treat .../index.html or .../*.html as files and remove them
  • If the result is /, basename becomes ""

Examples:

  • "/"""
  • "/app/""/app"
  • "/app/index.html""/app"

1.3 basename and direct links

  • If basename is "", no <base> exists, and the initial pathname !== "/", it is an error

  • If basename is "/app":

    • "/app" and "/app/" are the same (app root)
    • "/app" matches only "/app" or "/app/..." (does not match "/appX")

2) internalPath specification

2.1 internalPath normalization

internalPath is always treated as an absolute path.

  • Add leading /
  • Collapse multiple slashes
  • Remove trailing / (except root /)
  • If empty, become /
  • In Router normalization, remove trailing *.html when present

Examples:

  • ""/
  • "products"/products
  • "/products/"/products
  • "///a//b/"/a/b

2.2 Get internalPath from URL

Obtain internalPath by matching URL Pathname with basename.

  • If pathname === basename, then internalPath = "/"
  • If pathname starts with basename + "/", then internalPath = pathname.slice(basename.length)
  • Otherwise internalPath = pathname
  • If the slice result is "", then internalPath = "/"

Examples (basename=/app):

  • pathname=/app → internalPath=/
  • pathname=/app/ → internalPath=/
  • pathname=/app/products/42 → internalPath=/products/42

3) <wcs-route path="..."> specification

3.1 path notation

path follows internalPath rules.

  • Root (top-level) is "/"

  • Child routes allow relative paths (recommended)

    • Example: parent /products, child ":id"/products/:id

In implementation, paths are converted to absolute during parsing.

3.2 Matching rules

  • Exact match by segment
  • Parameter :id matches a single segment
  • Catch-all * matches the remaining path (accessible via params['*'])

3.3 Priority (longest match definition)

If multiple candidates exist, pick the higher priority:

  1. More segments
  2. If same, more static segments ("users" > ":id" > "*")
  3. If still same, definition order

Catch-all * has the lowest priority, so more specific routes always take precedence.

Example:

  • /admin/users/:id (static2 + param1)
  • /admin/users/profile (static3) → latter wins

3.4 Trailing slash

  • Matching is done after internal normalization, so

    • /products and /products/ are treated the same (either URL is OK)

3.5 Catch-all (*)

Specify * at the end of a path to match the entire remaining path.

<wcs-route path="/admin/profile"></wcs-route>  <!-- Priority -->
<wcs-route path="/admin/*"></wcs-route>        <!-- Fallback for /admin/xxx -->
<wcs-route path="/*"></wcs-route>              <!-- Last resort -->

| Path | Match | Reason | |------|-------|--------| | /admin/profile | /admin/profile | More segments | | /admin/setting | /admin/* | * matches setting | | /admin/a/b/c | /admin/* | * matches a/b/c | | /other | /* | Top-level catch-all |

The matched remaining path is accessible via params['*'].


4) <wcs-link to="..."> specification

4.1 When to starts with /

to is treated as internalPath.

  • The actual href is created by joining basename + internalPath
  • Join: "/app" + "/products""/app/products" (no //)

4.2 When to does not start with /

Treated as an external URL (new URL(to) is expected to succeed).

  • Example: https://example.com/

5) “Drop HTML files” is limited

Dropping .html only applies when the pathname actually looks like a file.

  • "/app/index.html""/app" (OK)
  • "/products""/" is NG (do not drop segments)