Published 2025-11-11, About 8 minute read.
URLPattern just became available in all browsers:
So I wanted to dig into what it would take to make a simple SPA router with vanilla JavaScript and browser APIs. We should be able to make a component that takes in a router configuration and renders the appropriate component determined by the browser URL.
What does URLPattern() Do? 📎
Conditionally rendering components is not the difficult part with routers. The
difficult part is accurately testing the browser URL to determine which
component should render. And not only that, we have to be able to capture
dynamic parts of routes (think something like /posts/{post_id}.)
So, without further ado, here are some examples showing how to test if a route URL matches a pattern! You can then use this mechanism to create a router with easily configured paths.
You might be surprised by the fourth example above. There is a distinction
between /cat and /cat/. So to handle this, you can
make the pattern optionally include an ending forward slash in a group with
curly brackets and mark it optional with ?:
Yet another surprise! You may expect that you can accept more after
/cat/. To do that, include a wildcard asterisk:
Where do we start? 📎
I'm going to use an array of config objects that tie a URL route to specific web components. It's very similar to how you can create routers with vue-router.
The order of the config objects matters. We'll be testing each pattern one by one, and if we find a match, we'll render that web component.
How do we do the rendering? That would be the job of the web component we're placing all this logic in. The component will look at the current window URL, test that against all the router configs that we set up with URLPattern, and create and render the appropriate web component as a child.
Some frameworks call this router the "outlet" component.
Of course, you will need to have the web components my-home,
my-posts, and my-about registered as well.
But there we have it - a router that will render the appropriate web component on page load. We have much work to do, though. What if someone clicks a link? What if someone uses the browser to navigate forward or backward? We need to handle those, and luckily it's not too difficult.
Handling SPA Navigation and Link Clicks 📎
One thing to realize is that if you navigate to
http://www.myblog.com/some/path the server is going to normally
try to resolve /some/path on the back end. It might actually look
for the folders "some" and "path". But in a SPA we don't have folders- we have
one index HTML file that handles virtual paths. It's all done in the
client in JS! No matter which path we're going to, the server really only has
to serve up the index page. The client will then take over, use our new
URLPattern and handle rendering the appropriate component.
For configuring Vite to do this, it's dead simple. You can use a configuration
called spa. Just update your vite config:
This works well for your Vite local server. Unfortunately it's going to depend on where you deploy and what other frameworks/dev servers you're using. For some places like netlify you'll need to set a redirect rule in your netlify config. You may have to consult Stack Overflow, Google, or your LLM of choice to figure out how to do this for your particular situation.
But once you've got your server configuration and redirects in place, we can then start handling clicks!
We want to basically coopt any link clicks, and stop the browser from
navigating normally. This means we preventDefault() all click
events, and pull the target of the link out to test against our URL patterns.
This lets us know which component to render, and we set the URL manually to be
what the anchor tag was pointing to. It looks like we're changing pages, but
really we're simulating a page transition.
We need to set up a click handler when our router component connects to the DOM:
Of course, make sure you tear down these handlers in the
disconnectedCallback!
The user sees the URL change, the page transition, and even a new entry in the browser navigation history when a user clicks on a link. Now, we need to make sure that the browser doesn't actually go back or forward, but hooks into our router when back or forward buttons are clicked.
One last detail: browser navigation 📎
When a browser goes back or forward either programmatically (with
window.back()/forward()) or from a user clicking the back/forward
buttons, an event gets emitted: popstate.
The nice thing about this event is that the browser already moves back/forward
to the entry that we've been pushing to the history stack when we used
window.pushState. In other words, we just have to listen when
this back/forward navigation happens, and render our component. After
back/forward is pressed, the URL is already updated
So here is the final piece for our minimal viable router:
A working example 📎
If you're interested in seeing it work, here's an example built out in StackBlitz.