DEV Community

Cover image for Why I Built an SPA Router for HTML and Web Components
Aleksandr Buryakov for Aura UI

Posted on

Why I Built an SPA Router for HTML and Web Components

I didn't set out to build a router.

I had a project built with HTML and Web Components. It worked, and I was happy with that stack. Then a few sections needed client-side navigation.

Most pages needed to remain HTML-first for SEO and the initial render, so rewriting the entire site in React didn't make sense.

I added React only to those sections.

It solved the routing problem, but now I had React and Web Components in the same project. Maintaining both stacks became the new problem.

React wasn't the problem. The combination was.

At some point I started asking a simpler question: if the browser already has a component model, why do I need a second one just to get SPA navigation?

I wanted a router that treated HTML and Web Components as the application—not as legacy markup waiting to be replaced.

I looked for one. I couldn't find anything that matched the model I had in mind, so I started building Aura Router.

That became the use case for Aura: adding SPA navigation to an existing HTML or Web Components project without introducing a second component model.

The part I did not want to throw away

What I didn't want to throw away was the existing HTML and Web Components stack. The router had to fit that stack, not replace it. In practice, that meant:

  • every public URL should still return complete, indexable HTML from the server or static host;
  • routes should be declared in HTML as Custom Elements;
  • navigation should use ordinary <a href> links that still work without JavaScript;
  • Web Components in shared layouts should stay mounted when only the child route changes;
  • the router should be framework-independent and usable anywhere Custom Elements work.

Those requirements led to a simple model: the host keeps serving complete pages. Aura can reuse the HTML already on screen, then handle marked links without loading a new document. Unmarked links keep their normal browser behavior.

That was the idea. But I still needed a way to prove that the browser was not reloading the page. So I made the demo show exactly what was happening.

A demo you can verify

Open the live demo and look at the strip labeled Live navigation proof.

Aura Router navigation proof showing one full page load, a persistent Load ID, and nine client transitions

It assigns a Load ID to the current document and counts client transitions. Click How it works.

The URL changes. The heading changes. The client transition counter goes up. The Load ID doesn't change.

Now reload the page. The Load ID changes.

That's the whole idea in one small experiment: Aura changed the page, but the browser did not load a new document.

There is another test I care about just as much. Disable JavaScript, reload the demo, and use the same navigation. The transitions become ordinary page loads, but the pages and links still work.

You can also use View Page Source. The content is already in the response; the demo is not an empty app shell waiting to be rendered.

What this looks like in code

Existing links keep their href. Add one attribute when a link should use client navigation:

<a href="/about/" aura-router-link>About</a>
Enter fullscreen mode Exit fullscreen mode

Routes are declared in HTML. path defines which URL a route matches, while view defines what that route should load. <aura-outlet> provides the target for the active view:

<main id="content">
  <h1>Home</h1>
</main>

<aura-outlet></aura-outlet>

<aura-router extract="#content">
  <aura-route path="/" view="/"></aura-route>
  <aura-route path="/about/" view="/about/"></aura-route>
</aura-router>
Enter fullscreen mode Exit fullscreen mode

Install the current release:

npm install --save-exact @auraui/router@0.1.0
Enter fullscreen mode Exit fullscreen mode

In JavaScript, one call registers Aura's Custom Elements:

import { AuraRouter } from "@auraui/router";

AuraRouter.install();
Enter fullscreen mode Exit fullscreen mode

When view points to an HTML page, the optional extract selector narrows the response to a single element.

The same extract selector is used on the initial page load: Aura adopts the matching #content element already in the document instead of loading the current view again.

The complete runnable example is available in StackBlitz and in the 10-minute walkthrough.

Swapping a page was only the first test

The flat example solves basic navigation. But I also wanted shared Web Components to survive child route changes.

Suppose a workspace has a sidebar with local state, event listeners, or expensive setup. If only the child URL changes, remounting the sidebar would throw away its component instance and state.

To avoid that remount, Aura keeps the shared UI in a parent layout and renders child routes into its outlet:

<template id="workspace-shell">
  <workspace-sidebar></workspace-sidebar>
  <aura-outlet></aura-outlet>
</template>

<aura-route path="/workspace/" layout="workspace-shell">
  <aura-route path="." view="/workspace/"></aura-route>
  <aura-route path="settings" view="/workspace/settings/"></aura-route>
</aura-route>
Enter fullscreen mode Exit fullscreen mode

When navigation stays inside /workspace/, Aura keeps the matched parent layout mounted and changes only the nested outlet.

I made that visible in the nested demo. Open it, note the Layout ID, and then click Settings. The child content changes, but the Layout ID does not.

This is the part that mattered for Web Components: their instances can stay alive while navigation changes only the child view.

One current boundary is worth calling out: the server or static host still owns the initial response. Today, a direct nested URL must return the parent layout, nested outlet, and child content; Aura does not generate that backend markup.

Why publish at 0.1?

Aura Router is currently at 0.1.0. Core navigation, first-paint adoption, and nested layouts are implemented and tested, but the public API may still change before 1.0. I recommend pinning the exact version.

I've tested the cases I know. The gaps I care about now are the ones that show up in projects I didn't build: awkward server setups, integration edge cases, and places where the HTML-first model does not fit. Publishing now gives that feedback a chance to shape the API before 1.0.

This post focuses on why Aura exists and how its core model works. The guide documents the API available today; I plan to publish a deeper API walkthrough as it stabilizes toward 1.0.

Try Aura Router:

  • Live demo — verify the navigation behavior
  • StackBlitz — explore the runnable project
  • GitHub — source, documentation, and issues
  • npm — install @auraui/router

If you try Aura in an existing HTML or Web Components project, I'd like to know what worked and what got in your way. Share your experience in the comments or in GitHub Discussions.

Top comments (0)