@beforesemicolon/router

Routing
in plain HTML.

A tiny Web Component router for HTML-first apps. Route by pathname or search query, lazy-load HTML/JS/TXT pages, nest route layouts, guard protected screens, and keep framework lock-in out of your navigation layer.

5 WEB COMPONENTS
0 CONFIG
HTML JS - TXT PAGES
index.html
html
1<!-- index.html — zero JavaScript required -->2<nav>3    <page-link path="/" title="Home">Home</page-link>4    <page-link path="/todos" title="Todos">Todos</page-link>5    <page-link path="/contact" title="Contact">Contact</page-link>6</nav>7 8<page-route path="/">9    <h1>Welcome</h1>10</page-route>11 12<!-- lazy-load HTML, txt or JS files -->13<page-route path="/todos" src="./pages/todos.html"></page-route>14<page-route path="/contact" src="./pages/contact.js"></page-route>15 16<!-- redirect unknown paths -->17<page-redirect path="/404"></page-redirect>

Powered by Markup & Web Component.

Router is built on top of Web Component, which is built on top of Markup. Same engine, modular packages, zero lock-in.

Markup

@beforesemicolon/markup

The 9Kb reactive templating system that powers everything. Tagged templates, state, effects, repeat, suspense.

Read the docs

Web Component

@beforesemicolon/web-component

A reactive layer over native Custom Elements — the foundation Router is built on. Props, state, scoped styles, lifecycles.

Read the docs

Routing that disappears into HTML.

Five web components, one central matcher, and a clean JS API for everything you can't express in markup.

Plug & play

Drop in two script tags and route with HTML. No build step, no router config, no JavaScript required.

Tiny & focused

A small surface area: five web components and a JS API. Built on Markup + Web Component, with zero extra deps.

Lazy-loaded pages

Point page-route at an HTML, txt or JS file. Modules load once, cache between visits, mount and unmount cleanly.

Nested & named routes

Children extend their parent's path. Use the name attribute for switch-like, mutually exclusive matching.

Search-query routing

page-route-query renders content based on ?key=value — tabs, filters and modals without a single line of state.

Smart redirects

page-redirect targets unknown paths only — or always, scoped to its parent route. Perfect for 404s and defaults.

One match, one render

A central matcher resolves each navigation once. Guards run once. Subscribers only fire when they're relevant.

Cached remounts

Inactive routes are detached from the DOM but kept warm. Coming back is an instant remount, not a rebuild.

Works with any builder

Vite, Webpack, esbuild, plain HTML, or a CDN script tag — it's just web components, so it slots in anywhere.

Five examples. Infinite sitemaps.

Compose page-route, page-link, page-route-query, page-redirect, page-data and the small JS API around them.

EXAMPLE 01
Nested routes & params
app.html
html
1<!-- nested routes — child paths extend the parent -->2<page-route path="/projects" exact="false">3    <h1>Projects</h1>4 5    <!-- /projects -->6    <page-route src="./pages/projects-list.js"></page-route>7 8    <!-- /projects/:projectId — pathname params -->9    <page-route path="/:projectId" src="./pages/project.js">10        <div slot="loading">Loading project...</div>11        <div slot="fallback">Could not load this project.</div>12    </page-route>13 14    <!-- only redirects unknown CHILD routes of /projects -->15    <page-redirect path="/projects/not-found"></page-redirect>16</page-route>
EXAMPLE 02
Search-query routes
tabs.html
html
1<!-- routing by ?tab= — perfect for tabs, filters, modals -->2<div class="tabs">3    <div class="tab-header">4        <page-link search="tab=one">Tab 1</page-link>5        <page-link search="tab=two">Tab 2</page-link>6    </div>7 8    <div class="tab-content">9        <page-route-query key="tab" value="one">10            Tab One content11        </page-route-query>12 13        <page-route-query key="tab" value="two">14            Tab Two content15        </page-route-query>16    </div>17</div>
EXAMPLE 03
Page metadata
user.html
html
1<!-- render location metadata: payload, params and search queries -->2<page-link path="/users/42" title="User profile" payload='{"role": "admin"}'3    >Open user 42</page-link4>5 6<page-route path="/users/:userId">7    <h1>User <page-data param="userId">unknown</page-data></h1>8 9    <!-- payload data passed via the link -->10    <p>Role: <page-data key="role">guest</page-data></p>11 12    <!-- current search query value (following router API search-param attribute) -->13    <p>Tab: <page-data search-param="tab">overview</page-data></p>14</page-route>
EXAMPLE 04
Protected routes
guards.js
javascript
1import {2    registerGlobalGuard,3    registerRouteGuard,4} from '@beforesemicolon/router'5 6registerGlobalGuard((pathname) => {7    const publicPages = ['/', '/login', '/pricing', '/404']8 9    if (!publicPages.includes(pathname) && !auth.isSignedIn()) {10        return '/login'11    }12 13    return true14})15 16registerRouteGuard('/admin/:section', async () => {17    return (await auth.hasRole('admin')) || '/unauthorized'18})
EXAMPLE 05
Bundler-friendly pages
routes.js
javascript
1import { registerRouteModules } from '@beforesemicolon/router'2 3registerRouteModules(4    import.meta.glob('./pages/**/*.{js,html,txt}', {5        eager: false,6    })7)

Install in seconds.

Choose your preferred installation method. Works everywhere JavaScript runs.

<script src="https://unpkg.com/@beforesemicolon/router/dist/client.js"></script>
npm install @beforesemicolon/router
yarn add @beforesemicolon/router
pnpm add @beforesemicolon/router

Build single and multi-page apps, your way.

Combine the simplicity of vanilla Web Standards with the power of declarative routing.