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

spark-html-router

v1.0.0

Published

Declarative <template route> client routing for spark-html — no JS config, just markup.

Readme

⚡ spark-html-router

Declarative client routing for spark-htmlno JS config, just markup. Write your routes as <template route> blocks and call router() once.

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a href="/projects">Projects</a>
</nav>

<template route="/">         <div import="components/home"></div>      </template>
<template route="/about">    <div import="components/about"></div>     </template>
<template route="/projects"> <div import="components/projects"></div>  </template>
<template route="*">         <div import="components/not-found"></div> </template>

<script type="module">
  import { router } from 'spark-html-router';
  router();          // that's it
</script>

router() mounts the page once (chrome + the active route together — every component's onMount fires exactly once), shows the <template route> that matches the URL, intercepts same-origin <a> clicks for SPA navigation (no full reload), and tracks Back/Forward. The route templates are inert to the core runtime, so this is a tiny add-on — the spark-html core stays router-free.

Active links (zero config)

After every navigation the router sets aria-current="page" on the <a> whose href matches the current route, and clears it from the rest. Highlight the active link with pure CSS — no useStore, no per-link expressions:

<!-- components/site-nav.html -->
<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a href="/projects">Projects</a>
</nav>

<style>
  nav a[aria-current="page"] { color: #fff; font-weight: 700; }
</style>

Reactive active route

For anything beyond link styling — the document title, analytics, a breadcrumb — the router also publishes the current path to a built-in route store, so any component can react with useStore('route') (no popstate/pushState wiring):

<script>
  const route = useStore('route');
  $: document.title = route.path === '/' ? 'Home' : 'My Site';
</script>

Focus & scroll on navigation (a11y, zero config)

On a forward navigation the router moves keyboard/screen-reader focus into the newly rendered view (so users aren't stranded at the top of the old page) and resets scroll — to the #hash target if the URL has one, otherwise to the top. Back/Forward (popstate) is left alone: the browser restores its scroll position and focus isn't yanked.

By default the view's root receives focus. To choose a better target (e.g. the page heading), mark it:

<template route="/about">
  <h1 data-router-focus tabindex="-1">About</h1>
  …
</template>

([autofocus] works too.) The router adds a temporary tabindex="-1" if needed and removes it on blur, so nothing lingers in the DOM or the tab order.

Install

bun add spark-html-router

API

import { router, navigate } from 'spark-html-router';

await router({ base: '/spark' });   // base = path prefix (e.g. GitHub Pages)
navigate('/about');                 // navigate programmatically

| Option | Meaning | |--------|---------| | base | Path prefix the app is served under (e.g. /spark). Stripped before matching, added back when navigating. | | root | Mount root (default document.body). |

Routes

  • Exact matchroute="/about" matches /about (trailing slashes and the base path are normalized away).
  • Dynamic segmentsroute="/blog/:id" matches /blog/42; the captured params land on the route store. Exact routes win over dynamic ones.
  • Catch-allroute="*" renders for any unmatched path (a 404 page).
  • Default 404 — if the page declares no route="*", the router injects a minimal built-in not-found view (a 404 heading + a link home), so unknown URLs never render a blank page. Declare your own route="*" to replace it. With spark-prerender, a 404.html is also generated automatically at build time (your own 404.html, e.g. from public/, always wins).
<template route="/blog/:id"><div import="components/post"></div></template>

<!-- components/post.html -->
<h1>Post #{post}</h1>
<script>
  const route = useStore('route');
  $: post = route.params.id;      // "42" on /blog/42
</script>

Precedence: exact → dynamic → catch-all. Navigating between two matches of the same dynamic route (/blog/1/blog/2) re-mounts the route with the new params.

Query string — route.query

The URL's search params are a plain reactive object on the route store — no manual location.search parsing, no popstate wiring:

<script>
  const route = useStore('route');
  let page = 1;
  $: page = Number(route.query.page || 1);      // ?page=2 → 2

  function next() {
    route.query.page = String(page + 1);        // updates the URL bar + re-renders
  }
</script>
  • Reading: route.query mirrors URLSearchParams as { page: "2", q: "hi" } (values are strings, like the platform gives them).
  • Writing route.query.page = "3" updates the URL in place via replaceState — shareable state with no navigation and no history entry.
  • Setting a param to null/undefined/'' removes it from the URL.
  • navigate('/projects?page=2') works; navigating without a query string clears route.query (the URL is the source of truth).

Nested routes & layouts

Nest <template route> blocks to build a persistent layout with swappable children. A parent route renders whenever the URL is under it; the matching child renders wherever you place the nested templates:

<template route="/dash">
  <div import="components/dash-layout"></div>   <!-- sidebar, header… -->
  <main>
    <template route="/dash">          <div import="components/dash-home"></div>     </template>
    <template route="/dash/settings"> <div import="components/dash-settings"></div> </template>
  </main>
</template>

The parent layout is kept alive across child navigations — its components aren't re-mounted, so layout state (open menus, scroll, form input) survives. Only the part of the tree that actually changed is rebuilt. Precedence per level is still exact → dynamic → longest layout-prefix → catch-all.

SEO / prerender

Pair it with spark-prerender: it discovers your <template route> routes at build time and emits one fully-rendered HTML file per route (about.html, projects.html, …) plus the host rewrite rules — so crawlers get real content per URL, and the client adopts the prerendered route with no flash.

Notes

  • Covers exact routes, dynamic :param segments, nested routes/layouts, and a catch-all. Dynamic routes render on the client (their params aren't known at build time, so spark-prerender skips them); concrete top-level routes prerender as usual (nested children render on the client). Wildcard splats (/docs/*) are not yet supported.

The Spark family

Small, single-purpose packages that share one philosophy: no compiler, no virtual DOM, no build step required — built for humans who love hand-writing their web apps. Add only what you use.

| Package | What it does | |---|---| | spark-html | The runtime — components, reactivity, stores, forms, scoped styles. ~14.4 kB gzip, 0 deps. | | spark-html-bun | Dev server, bundler & preview on Bun — scoped HMR, no-build dev, post-build pipeline. | | spark-html-router | <template route> routing — nested routes/layouts, route.query, active links. | | spark-html-theme | Dark/light/system theming in one line — persisted, no flash. | | spark-html-head | Reactive <title>/<meta> per route + a head store. | | spark-html-motion | Enter/leave transitions on if/each blocks — transition="fade|slide|scale". | | spark-html-devtools | In-page devtools — live stores, component tree, patch activity. | | spark-html-query | Declarative async data — a self-fetching store (loading/error/data/refetch). | | spark-html-persist | Persist stores to localStorage/sessionStorage in one line. | | spark-html-websocket | A WebSocket as a reactive store — auto-reconnect, JSON, send(). | | spark-prerender | Build-time SEO prerender + sitemap/robots — no SSR server. | | spark-ssr | Full-stack SSR on Bun — the template is the backend: inferred DB, REST CRUD, auth, live updates. Precompiled + response-cached: fast by default. | | spark-html-image | Build-time image optimization — webp/avif + responsive srcset, zero config. | | spark-html-font | Font loading optimizer — preload + size-adjusted fallbacks, no FOUT. | | spark-html-manifest | PWA manifest + icons + head tags (and optional service worker) from one config. | | spark-html-offline | Offline URL imports — a service worker that caches CDN components. | | spark-html-sri | Subresource Integrity — hash + verify assets and remote components. | | create-spark-html-app | Scaffold a spark-html app in one command. | | prettier-plugin-spark | Prettier for components — formats <script>/<style>, markup stays byte-for-byte. | | spark-html-language-server | LSP — diagnostics, go-to-definition, prop autocomplete, hover docs. |