Skip to content

Repository files navigation

Texivia

1.2 kB router. Zero dependencies. Any framework.

From Latin textor (weaver) + via (road) — "path weaver."

Frontend Routers — The Reality Check

Bundle Sizenpm versionTypeScriptLicense

npm install texivia-router

Why Texivia?

TanStackVue RouternavigowouterTexivia
LOC11,5612,5031,007420160
Gzipped31.6 kB3.5 kB2.9 kB2.5 kB1.4 kB
Files58328201
Deps717030
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.

Quick Start

import{Router}from'texivia-router';constrouter=newRouter([{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.

Svelte 5

Extract the router into its own module so any component or service can import it:

// src/router.tsimport{Router}from'texivia-router';importtype{Component}from'svelte';importHomefrom'./pages/Home.svelte';importRecipeDetailfrom'./pages/RecipeDetail.svelte';importNotFoundfrom'./pages/NotFound.svelte';exportconstrouter=newRouter<Component<any>>([{path: '/',view: Home},{path: '/recipe/{id}',view: RecipeDetail},{path: '*',view: NotFound},]);
<!-- App.svelte -->
<scriptlang="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', onNavigateasEventListener);return () => {router.stop();document.removeEventListener('texivia', onNavigateasEventListener); }; });</script>
{#ifView}
<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 componentimport{router}from'./router';router.navigate('/dashboard');

Nested layouts

Texivia doesn't need a nested routing concept. Use your framework's composition instead:

<!-- pages/RecipeDetail.svelte -->
<scriptlang="ts">importLayoutfrom'../components/layout/Layout.svelte';const { id } =$props();</script>
<Layout>
{#snippetbody()}
<h1>Recipe #{id}</h1>
<!-- page content -->
{/snippet}
</Layout>

Layouts are components, not router config. This keeps the router simple and your layouts flexible.

Vue 3

// src/router.tsimport{Router}from'texivia-router';importtype{Component}from'vue';importHomefrom'./pages/Home.vue';importRecipeDetailfrom'./pages/RecipeDetail.vue';importNotFoundfrom'./pages/NotFound.vue';exportconstrouter=newRouter<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';importHomefrom'./pages/Home.vue';const View =shallowRef(Home);const params =ref<Record<string, string>>({});function onNavigate(event:Event) {const detail = (eventasCustomEvent).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().

Features

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.

constrouter=newRouter([{path: '/dashboard',view: 'Dashboard',handler: async(match)=>{if(!awaitisAuthenticated())return'/login';awaitpreloadData(match.params);returntrue;}}]);

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.

<ahref="/recipes/42">Recipe</a><!-- intercepted --><ahref="https://example.com">Ext</a><!-- ignored: external --><ahref="/file.pdf" download>PDF</a><!-- ignored: download --><ahref="/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.

API

new Router<T>(config)

Creates a router instance. config is an array of route objects:

typeConfigRoute<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. Return true to proceed, false to cancel, or a string to redirect.

router.start(): Promise<void>

Starts the router. Attaches popstate, click, and texivia.goto listeners. Navigates to the current URL.

router.stop(): void

Removes all event listeners. Call on cleanup.

router.navigate(path): Promise<MatchedRoute<T> | null>

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.

awaitrouter.navigate('/recipe/42');awaitrouter.navigate('/search?q=pasta#results');

Event: texivia

Dispatched on document after each successful navigation.

typeMatchedRoute<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;});

Testing

Tested with Vitest covering route matching, navigation, handlers, redirects, link interception, and edge cases.

npm test

License

Apache 2.0

About

Lightweight (1.2 kB), framework-agnostic TypeScript router that uses combined regex-based path matching for flexible and powerful route handling

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages