Basic SPA
A full working example you can copy verbatim. Three pages: home, posts list, individual post. Lazy-loaded chunks. Active-link styling. ~70 lines of your code.
Project layout
src/├── main.ts # bootstrap├── routes.ts # route table├── nav.ts # active-link helper└── pages/ ├── home.ts ├── posts.ts └── post.tssrc/routes.ts
export const routes = [ { path: '/', handler: () => import('./pages/home') }, { path: '/posts', handler: () => import('./pages/posts') }, { path: '/posts/:id', handler: () => import('./pages/post') }, { path: '/*', handler: () => import('./pages/not-found') },] as const;src/main.ts
import { createRouter } from 'routekit';import { routes } from './routes';import { updateActiveLinks } from './nav';
const root = document.querySelector<HTMLElement>('#app')!;const router = createRouter(routes);
router.on('navigate', async ({ match }) => { const mod = await match.handler() as { default: PageRender }; mod.default(root, match.params); updateActiveLinks(router.url);});
router.start();
type PageRender = (root: HTMLElement, params: Record<string, string>) => void;src/nav.ts
import { isActive } from 'routekit';
export function updateActiveLinks(currentUrl: string): void { document.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((a) => { const href = a.getAttribute('href'); if (!href) return; a.toggleAttribute('data-active', isActive(href, currentUrl)); });}src/pages/home.ts
export default function render(root: HTMLElement) { root.innerHTML = ` <h1>routekit demo</h1> <nav> <a href="/">Home</a> <a href="/posts">Posts</a> </nav> `;}src/pages/posts.ts
const posts = [ { id: 1, title: 'In praise of small libraries' }, { id: 2, title: 'Why your bundle size matters' }, { id: 3, title: 'Type-safe URLs' },];
export default function render(root: HTMLElement) { root.innerHTML = ` <h1>Posts</h1> <ul>${posts.map((p) => ` <li><a href="/posts/${p.id}">${p.title}</a></li> `).join('')}</ul> `;}src/pages/post.ts
export default function render(root: HTMLElement, params: { id: string }) { root.innerHTML = ` <h1>Post #${params.id}</h1> <p>This is post number ${params.id}.</p> <a href="/posts">← Back to posts</a> `;}src/pages/not-found.ts
export default function render(root: HTMLElement) { root.innerHTML = ` <h1>Not found</h1> <p>That page doesn't exist.</p> <a href="/">Go home</a> `;}index.html
<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>routekit demo</title> <style> body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; } nav { display: flex; gap: 1rem; margin: 1rem 0; } nav a { color: #2563EB; text-decoration: none; } nav a[data-active] { font-weight: 600; text-decoration: underline; } </style> </head> <body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> </body></html>Run it
With Vite:
npm install vite routekitnpx viteVisit http://localhost:5173/. Click “Posts,” then click a post. Hit the back button. Notice the page transitions are instant (chunks are cached after first visit) and the active link follows your current URL.
That’s it — a typed, code-split, framework-agnostic SPA in under 100 lines of your code.