1.2 kB router. Zero dependencies. Any framework.
From Latin textor (weaver) + via (road) — "path weaver."
npm install texivia-router
| TanStack | Vue Router | navigo | wouter | Texivia | |
|---|---|---|---|---|---|
| LOC | 11,561 | 2,503 | 1,007 | 420 | 160 |
| Gzipped | 31.6 kB | 3.5 kB | 2.9 kB | 2.5 kB | 1.4 kB |
| Files | 58 | 32 | 8 | 20 | 1 |
| Deps | 7 | 17 | 0 | 3 | 0 |
| Svelte | – | – | ✓ | – | ✓ |
| React | ✓ | – | ✓ | ✓ | ✓ |
| Vue | – | ✓ | ✓ | – | ✓ |
| Angular | – | – | ✓ | – | ✓ |
| Vanilla | – | – | ✓ | – | ✓ |
Texivia compiles all routes into a single regex at startup. Matching is a single exec() call — O(1) regardless of route count.
import { Router } from 'texivia-router';
const router = new Router([
{ path: '/', view: Home },
{ path: '/recipe/{id}', view: RecipeDetail },
{ path: '*', view: NotFound }
]);
router.start();
document.addEventListener('texivia', (e) => {
const { view, params, search, hash } = e.detail;
// render your app — view is whatever you put in the config
});That's it. No providers, no wrappers, no context.
Extract the router into its own module so any component or service can import it:
// src/router.ts
import { Router } from 'texivia-router';
import type { Component } from 'svelte';
import Home from './pages/Home.svelte';
import RecipeDetail from './pages/RecipeDetail.svelte';
import NotFound from './pages/NotFound.svelte';
export const router = new Router<Component<any>>([
{ path: '/', view: Home },
{ path: '/recipe/{id}', view: RecipeDetail },
{ path: '*', view: NotFound },
]);<!-- App.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { router } from './router';
let View = $state(null);
let params = $state({});
function onNavigate(event: CustomEvent) {
View = event.detail.view;
params = event.detail?.params || {};
}
onMount(() => {
router.start();
document.addEventListener('texivia', onNavigate as EventListener);
return () => {
router.stop();
document.removeEventListener('texivia', onNavigate as EventListener);
};
});
</script>
{#if View}
<View {...params} />
{/if}The view property holds the Svelte component directly — no string-to-component lookup. Router<Component<any>> gives you full type safety across the config.
No <Link> components. Plain <a> tags just work — Texivia intercepts relative links automatically. For programmatic navigation, import the router:
// anywhere — a service, a handler, another component
import { router } from './router';
router.navigate('/dashboard');Texivia doesn't need a nested routing concept. Use your framework's composition instead:
<!-- pages/RecipeDetail.svelte -->
<script lang="ts">
import Layout from '../components/layout/Layout.svelte';
const { id } = $props();
</script>
<Layout>
{#snippet body()}
<h1>Recipe #{id}</h1>
<!-- page content -->
{/snippet}
</Layout>Layouts are components, not router config. This keeps the router simple and your layouts flexible.
// src/router.ts
import { Router } from 'texivia-router';
import type { Component } from 'vue';
import Home from './pages/Home.vue';
import RecipeDetail from './pages/RecipeDetail.vue';
import NotFound from './pages/NotFound.vue';
export const router = new Router<Component>([
{ path: '/', view: Home },
{ path: '/recipe/{id}', view: RecipeDetail },
{ path: '*', view: NotFound },
]);<!-- App.vue -->
<script setup lang="ts">
import { shallowRef, ref, onMounted, onUnmounted } from 'vue';
import { router } from './router';
import Home from './pages/Home.vue';
const View = shallowRef(Home);
const params = ref<Record<string, string>>({});
function onNavigate(event: Event) {
const detail = (event as CustomEvent).detail;
View.value = detail.view;
params.value = detail?.params || {};
}
onMounted(() => {
router.start();
document.addEventListener('texivia', onNavigate);
});
onUnmounted(() => {
router.stop();
document.removeEventListener('texivia', onNavigate);
});
</script>
<template>
<component :is="View" v-bind="params" />
</template>Same pattern — shared router.ts, import where you need router.navigate().
Compiled regex matching — All routes become one regex. One exec() per navigation, regardless of route count.
Framework-agnostic — Works with Svelte, Vue, React, or vanilla JS. No adapters, no plugins. Standard DOM events in, DOM events out.
Type-safe — Generic Router<T> lets you type your view data. Route configs, matched routes, and handler signatures are fully typed.
Dynamic parameters with constraints — {id} matches any segment. {id:\\d+} matches only digits. {slug:[a-z-]+} matches only lowercase slugs. Full regex power per segment.
Async navigation handlers — Per-route handler functions run before navigation. Return true to proceed, false to cancel, or a string to redirect. Supports async/await for auth checks, data loading, or analytics.
const router = new Router([
{
path: '/dashboard',
view: 'Dashboard',
handler: async (match) => {
if (!await isAuthenticated()) return '/login';
await preloadData(match.params);
return true;
}
}
]);Automatic link interception — Clicks on relative <a> tags are captured and routed. External links, target="_blank", download, and no-router attributes are ignored. No special link components needed.
<a href="/recipes/42">Recipe</a> <!-- intercepted -->
<a href="https://example.com">Ext</a> <!-- ignored: external -->
<a href="/file.pdf" download>PDF</a> <!-- ignored: download -->
<a href="/raw" no-router>Raw</a> <!-- ignored: opt-out -->Declarative redirects — { path: '/old', redirect: '/new' } in the config. No imperative redirect logic needed.
Catch-all 404 — { path: '*' } matches anything not matched by other routes. Place it last in your config.
Event-driven — Every navigation dispatches a texivia CustomEvent on document with full route detail: view, params, search, hash.
Programmatic navigation — Call router.navigate() from anywhere:
router.navigate('/recipes/42');
router.navigate('/search?q=pasta#results');History API — Uses pushState/popstate for clean URLs. No hash routing.
Nesting through composition — No nested route config. Use your framework's own layout/slot/snippet system. The router stays flat, your component tree stays flexible.
Creates a router instance. config is an array of route objects:
type ConfigRoute<T> = {
path: string;
view?: T;
redirect?: string;
handler?: (match: MatchedRoute<T>) => string | boolean | Promise<string | boolean>;
};path— URL pattern. Literal segments,{param}or{param:regex}for dynamic segments,*for catch-all.view— The view or component to render for this route.redirect— Target path for redirects.handler— Async or sync function called before navigation. Returntrueto proceed,falseto cancel, or a string to redirect.
Starts the router. Attaches popstate, click, and texivia.goto listeners. Navigates to the current URL.
Removes all event listeners. Call on cleanup.
Navigates to the given path. Runs handlers, pushes history state, and dispatches the texivia event. Returns the matched route or null if no route matches.
await router.navigate('/recipe/42');
await router.navigate('/search?q=pasta#results');Dispatched on document after each successful navigation.
type MatchedRoute<T> = {
path: string;
view?: T;
params: Record<string, string>;
search: Record<string, string>;
hash: string;
};
document.addEventListener('texivia', (e: CustomEvent<MatchedRoute<T>>) => {
const { view, params, search, hash } = e.detail;
});Tested with Vitest covering route matching, navigation, handlers, redirects, link interception, and edge cases.
npm test