Virou is a high-performance, lightweight virtual router for Vue with dynamic routing capabilities.
Perfect for modals, wizards, embeddable widgets, or any scenario requiring routing without altering the browser's URL or history.
- 🪄 Dynamic Virtual Routing: Navigate without altering the browser's URL or history
- 🍂 Multiple Router Instances: Manage independent routing contexts within the same app
- 🪆 Nested Routing: Seamlessly handle complex, nested routes
- 🦾 Type-Safe: Written in TypeScript
- ⚡ SSR-Friendly: Compatible with server-side rendering
<script setup lang="ts">
import { useVRouter } from '@virou/core'
// Define your routes
const routes = [
{
path: '/',
component: () => import('./views/Home.vue'),
},
{
path: '/about',
component: () => import('./views/About.vue'),
},
]
// Create a virtual router instance
const { router, route } = useVRouter(routes)
</script>
<template>
<!-- Renders the current route's component -->
<VRouterView />
</template>Install Virou with your package manager:
pnpm add @virou/coreRegister the virou plugin in your Vue app:
import { virou } from '@virou/core'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App)
.use(virou)
.mount('#app')
⚠️ Virou doesn’t globally register any components (includingVRouterView); it only adds a$virouglobal property to store router instances in aMap<string, VRouterData>, which are automatically removed when no longer in use.
Install Virou Nuxt module with your package manager:
pnpm add @virou/nuxtAdd the module to your Nuxt configuration:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@virou/nuxt',
],
})Declare your routes as an array of objects with required path and component, and optional meta and children properties.
const routes: VRouterRaw[] = [
{
path: '/', // static path
component: Home,
},
{
path: '/user/:id', // dynamic path with parameter
component: () => import('./views/User.vue'),
meta: {
foo: 'bar',
}
},
{
path: '/**:notFound', // Named wildcard path
component: defineAsyncComponent(() => import('./views/NotFound.vue')),
}
]Props:
path: the URL pattern to match. Supports:- Static ("/about") for exact matches
- Dynamic ("/user/:id") for named parameters
- Wildcard ("/**") for catch-all segments
- Named wildcard ("/**:notFound") for catch-all with a name
component: the Vue component to render when this route matches. Can be synchronous or an async loader.meta: metadata for the route.children: an array of child routes.
Virou uses rou3 under the hood for create router context and route matching.
To define nested routes, add a children array to a route record. Child path values are relative to their parent (leading / is ignored).
const routes: VRouterRaw[] = [
// ...
{
path: '/user/:id',
component: User,
children: [
{
path: '', // /user/:id -> default child route
component: UserProfile,
},
{
path: '/settings', // /user/:id/settings
component: UserSettings,
children: [
{
path: '/', // /user/:id/settings -> deep default child route
component: UserSettingsGeneral,
},
{
path: '/notifications', // /user/:id/settings/notifications
component: UserSettingsNotifications,
},
],
},
]
},
// ...
]Create (or access) a virtual router instance with useVRouter composable.
const { router, route } = useVRouter('my-wizard', routes)
useVRoutermust be called insidesetup().
Params:
key: a unique key for the router instance. If you do not provide a key, Virou will generate one viauseId().routes: an array of route objects.options:initialPath: the path to render on initialization (defaults to/).
Returns:
route: a Vue shallow ref that contains the current route object.export interface VRoute { fullPath: string path: string search: string hash: string meta?: Record<PropertyKey, unknown> params?: Record<string, string> '~renderList': Component[] | null }
router:replace(path: string): void: navigate to a new path.addRoute(route: VRouteRaw): void: add a route at runtime.
Virou allows you to create multiple independent router instances within the same app.
<script setup lang="ts">
// Settings modal router
useVRouter('settings-modal', [
{
path: '/profile',
component: UserProfile,
},
{
path: '/preferences',
component: UserPreferences,
},
// ...
], { initialPath: '/profile' })
// Onboarding wizard router
useVRouter('onboarding-wizard', [
{
path: '/profile',
component: OnboardingProfile,
},
{
path: '/teamspace',
component: OnboardingTeamspace,
},
// ...
], { initialPath: '/profile' })
</script>
<template>
<SettingsModal>
<VRouterView router-key="settings-modal" />
</SettingsModal>
<Wizard>
<VRouterView router-key="onboarding-wizard" />
</Wizard>
</template><VRouterView> mounts the matched component at its current nesting depth.
<template>
<VRouterView router-key="my-router">
:keep-alive="true" <!-- preserve component state -->
:view-key="(route, key) => `${key}|${route.fullPath}`" <!-- custom vnode key -->
>
<!-- default slot receives { Component, route } -->
<template #default="{ Component, route }">
<Transition name="fade" mode="out-in">
<component :is="Component" v-bind="route.params" />
</Transition>
</template>
<!-- fallback slot shown while async loader resolves -->
<template #fallback>
<div class="spinner">
Loading...
</div>
</template>
</VRouterView>
</template>Props:
routerKey: key of the router instance to render. If not provided, uses the nearest router instance.keepAlive: wraps the rendered component in<KeepAlive>when set to true, preserving its state across navigations.viewKey: accepts either a string or a function(route, key) => stringto compute the vnode key for the rendered component.
Slots:
default: slot receives{ Component, route }so you can wrap or decorate the active component.fallback: receives{ route }and is displayed inside<Suspense>while an async component is resolving.
Virou wraps components in
<Suspense>by default. To combine<Suspense>with other components, see the Vue docs.
By default, routers created with useVRouter(key, routes) are disposable—they unregister themselves automatically once no components reference them.
To keep a router alive for your app’s entire lifecycle, register it as a global router.
Defined routers in the virou plugin options are registered as global routers:
createApp(App)
.use(virou, {
routers: {
'embedded-widget-app': {
routes: [
{ path: '/chat', component: () => import('./views/Chat.vue') },
{ path: '/settings', component: () => import('./views/Settings.vue') },
],
options: { initialPath: '/chat' }
},
// add more global routers here...
}
})
.mount('#app')Later:
const { router, route } = useVRouter('embedded-widget-app')You may also mark a router as global at runtime by passing the isGlobal option:
useVRouter(routes, { isGlobal: true })That router will stay registered even after components that use it unmount.
You can create a standalone virtual router with createVRouter:
export function useCustomRouter() {
const { router, route } = createVRouter(routes)
// Custom logic here...
}