vue-micro-router
| Install | |
|---|---|
npm install vue-micro-router |
|
| Latest Version: | 1.1.2 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Sep 9, 2026 |
| Links: | GitHub · npm |
vue-micro-router
Mobile-app-style navigation for Vue 3 — no URL paths, just screens.
Build apps that feel like native mobile — pages slide in/out with smooth transitions, dialogs stack as modals, and persistent HUD controls float above everything. No /users/:id URL routing. Just push('profile') and watch it animate.
Why not vue-router?
| vue-router | vue-micro-router | |
|---|---|---|
| Navigation model | URL-based (/path/:param) |
Segment stack (home → home/menu → home/menu/settings) |
| Page transitions | Manual (TransitionGroup) | Built-in slide/fade animations + per-route customization |
| Multiple visible pages | No (one route = one view) | Yes — stacked pages all render simultaneously |
| Modal dialogs | DIY | First-class with stacking, backdrop, focus trap |
| GUI overlays / HUD | DIY | First-class controls with auto-show/hide |
| Route guards | beforeRouteEnter etc. |
beforeEach, beforeEnter, beforeLeave + async |
| State passing | Query params / route params | Reactive useMicroState() bridge |
| Navigation history | Browser history API | Built-in canGoBack / historyBack() |
| Gesture navigation | None | Swipe-back from left edge |
| Type safety | Route params typing | Auto-typed via Register + useMicroRouter() |
| State persistence | None | serialize() / restore() |
| Nested routers | Nested <router-view> |
Independent <MicroRouterView nested> |
| Lifecycle hooks | beforeRouteEnter etc. |
onRouteEnter, onDialogEnter, onControlEnter |
| Use case | Websites, SPAs | Games, mobile-style apps, kiosks, wizards |
How it works
Pages stack as path segments — push('menu') slides a new page on top. push(-1) slides it back. No browser URL changes. Just screens animating like a native app.
push('menu') → [home] ← [menu slides in]
push('settings') → [home] [menu] ← [settings slides in]
push(-1) → [home] [menu slides out →]
Dialogs and controls layer on top — independently managed with their own lifecycle, stacking, and transitions.
Install
bun add vue-micro-router
# or
npm install vue-micro-router
Quick Start
<!-- app-plugin.ts — declare once, typed everywhere -->
<script lang="ts">
import { defineFeaturePlugin } from 'vue-micro-router';
import HomePage from './pages/HomePage.vue';
import MenuPage from './pages/MenuPage.vue';
import SettingsPage from './pages/SettingsPage.vue';
import ConfirmDialog from './dialogs/ConfirmDialog.vue';
import MainGUI from './controls/MainGUI.vue';
export const appPlugin = defineFeaturePlugin({
name: 'app',
routes: [
{ path: 'home', component: HomePage },
{ path: 'menu', component: MenuPage, transition: 'fade', transitionDuration: 300 },
{ path: 'settings', component: SettingsPage, preload: 'adjacent',
beforeLeave: () => confirm('Leave settings?') },
],
dialogs: [
{ path: 'confirm', component: ConfirmDialog, activated: false },
],
controls: [
{ name: 'main_gui', component: MainGUI, activated: false },
],
} as const);
// Register plugin type globally — no generics needed in useMicroRouter()
declare module 'vue-micro-router' {
interface Register {
plugin: typeof appPlugin;
}
}
</script>
<!-- App.vue — fully typed without generics -->
<script setup>
import { MicroRouterView } from 'vue-micro-router';
import 'vue-micro-router/styles';
import { appPlugin } from './app-plugin';
const config = {
defaultPath: 'home',
defaultControlName: 'main_gui',
history: { enabled: true, maxEntries: 50 },
gesture: { enabled: true },
guards: {
beforeEach: [(to, from) => {
console.log(`Navigating: ${from} → ${to}`);
return true;
}],
},
};
</script>
<template>
<MicroRouterView :config :plugins="[appPlugin]" />
</template>
Core Concepts
Segment-Based Paths
Pages stack as path segments. "home/menu/settings" renders 3 pages simultaneously — each page slides in on top of the previous one.
const { push } = useMicroRouter();
// Push a new page on top
await push('menu'); // home → home/menu
// Push with absolute path
await push('/home/settings'); // → home/settings
// Go back one step
await push(-1); // home/settings → home
// Go back with props (forces remount)
await push(-1, { reset: true });
Navigation with Props
Pass data to pages via push() and read it with useMicroState():
// Parent — push with props
await push('profile', { userId: 42 });
<!-- ProfilePage.vue -->
<script setup>
import { useMicroState } from 'vue-micro-router';
// Read props passed via push() — auto-syncs mutations back to store
const { userId } = useMicroState<{ userId: number }>();
// With defaults for optional props
const { tab } = useMicroState({ tab: 'overview' });
</script>
Route Guards
Prevent or control navigation with sync/async guards:
// Global guards — run on every navigation
const config = {
guards: {
beforeEach: [
async (to, from) => {
if (to.includes('admin') && !isAuthenticated()) return false;
return true;
}
],
afterEach: [(to, from) => analytics.track('navigate', { to, from })],
}
};
// Per-route guards — on specific routes
defineFeaturePlugin({
routes: [
{
path: 'admin',
component: AdminPage,
beforeEnter: async (to, from) => {
const user = await fetchUser();
return user.isAdmin;
},
},
{
path: 'editor',
component: EditorPage,
beforeLeave: (to, from) => {
if (hasUnsavedChanges()) return confirm('Discard changes?');
return true;
},
},
],
});
Guard execution order: global beforeEach → target beforeEnter → source beforeLeave → global afterEach
Guards have a 5s timeout — if a guard doesn't resolve, navigation is cancelled.
Per-Route Transitions
Each route can have its own transition style and duration:
defineFeaturePlugin({
routes: [
{ path: 'home', component: HomePage }, // default: slide 500ms
{ path: 'settings', component: SettingsPage, transition: 'fade', transitionDuration: 300 },
{ path: 'modal-page', component: ModalPage, transition: 'none' }, // instant, no animation
],
});
Supported: 'slide' (default), 'fade', 'none'
Navigation History
Opt-in browser-like history with back/forward:
const config = {
history: { enabled: true, maxEntries: 50 },
};
// In any component:
const router = useMicroRouter();
router.canGoBack?.value; // reactive boolean
router.canGoForward?.value; // reactive boolean
await router.historyBack?.();
await router.historyForward?.();
await router.historyGo?.(-2); // go back 2 entries
History truncates forward entries on new push (browser behavior).
Type-Safe Routes
Register Pattern (Recommended)
Use Register module augmentation to declare your plugin type once. Then useMicroRouter() auto-infers everywhere — no generics needed.
// app-plugin.ts — declare once with `as const`
export const appPlugin = defineFeaturePlugin({
name: 'my-app',
routes: [
{ path: 'home', component: HomePage },
{ path: 'profile', component: ProfilePage },
],
dialogs: [{ path: 'confirm', component: ConfirmDialog, activated: false }],
controls: [{ name: 'main_hud', component: MainHUD, activated: false }],
} as const);
// Declare module once — this is the ONLY place you write the type
declare module 'vue-micro-router' {
interface Register {
plugin: typeof appPlugin;
}
}
Then everywhere:
// Any component — fully typed, zero generics
const { push, openDialog, toggleControl } = useMicroRouter();
push('profile'); // ✅ OK
push('typo'); // ❌ TS Error
openDialog('confirm'); // ✅ OK
toggleControl('main_hud', true); // ✅ OK
Benefits: Type-safe push/openDialog/closeDialog/toggleControl. No duplication. One declaration fixes all usages.
Manual Route Map (Alternative)
For simple cases or when you only need typed props (not route names):
interface AppRoutes {
home: undefined;
profile: { userId: number };
}
const router = useMicroRouter<AppRoutes>();
router.push('profile', { userId: 42 }); // ✅ OK
router.push('profile'); // ❌ TS Error: missing props
router.push('unknown'); // ❌ TS Error: unknown route
Untyped: useMicroRouter() without Register returns MicroRouterStore with untyped methods.
Typed Props (Per-Route/Dialog/Control Attrs)
Opt-in per-component — export interface Attrs to get typed push() / openDialog() / toggleControl() props.
Step 1: In your component, export interface Attrs:
<!-- ProfilePage.vue -->
<script setup lang="ts">
import { useMicroState } from 'vue-micro-router';
export interface Attrs {
userId: number;
username: string;
meta?: { title: string }; // optional fields OK
}
// Required fields need defaults (prevents undefined on remount)
const { userId, username, meta } = useMicroState<Attrs>({
userId: 0,
username: 'Guest',
});
// userId.value → number (never undefined)
// meta.value?.title → string | undefined (ref exists, value may be undefined)
</script>
<!-- ConfirmDialog.vue -->
<script setup lang="ts">
export interface Attrs {
title?: string;
message?: string;
onConfirm?: () => void;
}
const { title, message, onConfirm } = useMicroState<Attrs>({
title: 'Confirm',
message: 'Are you sure?',
});
</script>
Step 2: Run code generation:
npx vue-micro-router-gen # auto-detects src/ or app/, outputs src/vue-micro-router.d.ts
npx vue-micro-router-gen -d src # explicit scan directory
npx vue-micro-router-gen -o types/router.d.ts # custom output path
The script:
- Scans all
.tsfiles fordefineFeaturePlugin()calls (any folder structure) - Resolves
@/,~/,#/path aliases automatically - Finds
.vuecomponents withexport interface Attrs - Generates
vue-micro-router.d.tswith Register + AttrsMap augmentations
Step 3: Include in tsconfig and enjoy typed everywhere:
// tsconfig.json — add the generated file
{ "include": ["src/**/*.ts", "src/**/*.vue", "src/vue-micro-router.d.ts"] }
push('profile', { userId: 42, username: 'Danh' }); // ✅ typed, autocomplete
push('profile'); // ❌ TS error: missing required props
push('profile', { meta: { title: 'Hi' } }); // ✅ optional fields can be skipped
push('home'); // ✅ OK (no Attrs → untyped)
openDialog('confirm', { title: 'Sure?' }); // ✅ typed
Rules:
- Required fields in
Attrs→ must pass inpush()/openDialog() - Optional fields (
?) → can skip entirely, including the whole props arg - Routes without
export interface Attrs→push()accepts anyRecord<string, unknown> - Re-run
npx vue-micro-router-genafter adding/changingAttrsinterfaces
State Serialization
Save and restore full router state for session persistence:
const router = useMicroRouter();
// Save state (e.g., on page hide)
const snapshot = router.serialize!();
localStorage.setItem('router-state', JSON.stringify(snapshot));
// Restore state (e.g., on page load)
const saved = localStorage.getItem('router-state');
if (saved) await router.restore!(JSON.parse(saved));
Serializes: navigation path + attrs, dialog stack + attrs, control state + attrs.
Route Preloading
Preload async route components before they're needed:
defineFeaturePlugin({
routes: [
{ path: 'home', component: () => import('./Home.vue') },
{ path: 'shop', component: () => import('./Shop.vue'), preload: 'eager' }, // loads on mount
{ path: 'cart', component: () => import('./Cart.vue'), preload: 'adjacent' }, // loads after each nav
],
});
// Manual preload
await router.preloadRoute('cart');
Gesture Navigation (Swipe Back)
iOS-style swipe-back from the left edge:
const config = {
gesture: {
enabled: true,
edgeWidth: 20, // px from left edge (default: 20)
threshold: 0.3, // 30% screen width to trigger (default: 0.3)
velocityThreshold: 0.5, // px/ms fast swipe (default: 0.5)
},
};
Nested Routers
Independent router instances within the same component tree:
<template>
<!-- Root router -->
<MicroRouterView :config="rootConfig" :plugins="[mainPlugin]">
<!-- Inside a page component: -->
<MicroRouterView nested :config="tabConfig" :plugins="[tabPlugin]" />
</MicroRouterView>
</template>
// Access root router from within nested router
const rootRouter = useMicroRouter({ root: true });
const localRouter = useMicroRouter(); // nearest parent
Step-Wise Navigation
Animate through intermediate pages one-by-one:
const { stepWisePush, stepWiseBack } = useMicroRouter();
// Walk through: home → home/onboarding → home/onboarding/step1
await stepWisePush('/home/onboarding/step1');
// Relative multi-segment paths are type-checked: every segment must be a
// known route, joined by `/`, in any order and to any depth.
await stepWisePush('onboarding/step1'); // ✅ each segment validated
await stepWisePush('onboarding/oops'); // ❌ compile error — unknown segment
// Step back through each page with animation
await stepWiseBack(3);
Dialogs
Open modal dialogs with props and handle close:
const { openDialog, closeDialog, closeAllDialogs } = useMicroRouter();
// Open with props
openDialog('confirm', {
title: 'Delete item?',
onConfirm: () => handleDelete(),
});
// Close specific dialog
closeDialog('confirm');
// Close all open dialogs
closeAllDialogs();
Dialog options:
registerDialog({
path: 'settings-modal',
component: SettingsModal,
activated: false,
position: 'right', // 'standard' | 'top' | 'right' | 'bottom' | 'left'
transition: 'slide', // 'fade' | 'slide' | 'scale'
transitionDuration: 400,
fullscreen: false,
persistent: true, // prevent close on backdrop click / Escape
seamless: true, // transparent background, no shadow
});
GUI Controls
Persistent overlay controls (HUDs, toolbars) that auto-manage visibility:
const { toggleControl } = useMicroRouter();
// Show inventory HUD (auto-hides default main_gui)
toggleControl('inventory', true, { filter: 'weapons' });
// Hide inventory (auto-restores main_gui)
toggleControl('inventory', false);
Lifecycle Hooks
iOS-style viewWillAppear / viewWillDisappear — available for routes, dialogs, and controls:
<script setup>
import { useRouteLifecycle } from 'vue-micro-router';
useRouteLifecycle({
onRouteEnter: () => console.log('Page is now the active (top) page'),
onRouteLeave: () => console.log('Page is no longer the active page'),
});
</script>
Also: useDialogLifecycle({ onDialogEnter, onDialogLeave }) and useControlLifecycle({ onControlEnter, onControlLeave }).
Feature Plugins
Bundle routes, dialogs, and controls into feature modules:
import { defineFeaturePlugin } from 'vue-micro-router';
export const shopPlugin = defineFeaturePlugin({
name: 'shop',
routes: [
{ path: 'shop', component: () => import('./ShopPage.vue'), preload: 'eager' },
{ path: 'cart', component: () => import('./CartPage.vue'), preload: 'adjacent' },
],
dialogs: [
{ path: 'buy-confirm', component: () => import('./BuyConfirm.vue'), activated: false },
],
controls: [
{ name: 'shop_hud', component: () => import('./ShopHUD.vue'), activated: false },
],
});
Analytics / Page Tracking
Hook into navigation events:
const config: MicroRouterConfig = {
tracker: {
trackPageEnter: (page, from, to) => analytics.track('page_view', { page }),
trackPageLeave: (page, from, to) => analytics.track('page_leave', { page }),
trackDialogEnter: (dialog) => analytics.track('dialog_open', { dialog }),
trackDialogLeave: (dialog) => analytics.track('dialog_close', { dialog }),
trackGuiEnter: (name) => analytics.track('gui_show', { name }),
trackGuiLeave: (name) => analytics.track('gui_hide', { name }),
},
};
Vue Devtools
Opt in with config.devtools: true — shows a "Micro Router" inspector tab with:
- Current path and page stack
- Open dialogs with attrs
- Active controls
- Navigation timeline events
const config: MicroRouterConfig = {
defaultPath: 'home',
devtools: import.meta.env.DEV, // keep it out of your production builds
};
Requires @vue/devtools-api (optional peer dependency); without it the load
warns and no-ops. The inspector is loaded with a dynamic import(), so it is a
separate chunk that is never fetched while the flag is off, and the core entry
carries none of it.
Off means never fetched, not absent from your build output: config.devtools
is read inside the library, so no bundler can prove it false and the chunk still
lands in your dist/ (measured: eager entry −2,753 B, total emitted +793 B).
Setting it from import.meta.env.DEV keeps devtools out of what production
runs, not out of what it ships.
Optional: Audio Manager
Background music tied to route BGM fields. Supports custom audio backends via AudioAdapter:
bun add howler
import { useAudioManager, HowlerAdapter } from 'vue-micro-router/audio';
const audio = useAudioManager({
volumeRef: ref(80),
urlResolver: (name) => `/assets/audio/${name}.mp3`,
// adapter: new HowlerAdapter(), // default — or provide your own AudioAdapter
});
Custom adapter:
import type { AudioAdapter } from 'vue-micro-router/audio';
class WebAudioAdapter implements AudioAdapter {
async play(src, options) { /* Web Audio API */ }
stop() { /* ... */ }
pause() { /* ... */ }
resume() { /* ... */ }
fade(from, to, duration) { /* ... */ }
isPlaying() { return false; }
state() { return 'unloaded'; }
cleanup() { /* ... */ }
}
const audio = useAudioManager({ adapter: new WebAudioAdapter() });
Styles
Import styles separately (not bundled with JS):
import 'vue-micro-router/styles';
Includes page slide/fade transitions, dialog animations, control fade transitions, and GUI layer positioning.
Development
bun run lint # ESLint check
bun run lint:fix # Auto-fix lint issues
bun test # Run all tests
bun run typecheck # TypeScript strict check
bun run build # Build package
bun run gen:types # Generate vue-micro-router.d.ts (local dev)
bun run dev:example # Run example app
bun run publish:npm # Bump version + build + publish + tag + release
For consumers:
npx vue-micro-router-gen # Generate type augmentations in consumer project
API Reference
Composables
| Composable | Description |
|---|---|
useGlobalMicroRouter(config?) |
Create & provide store. Call once in root. |
useMicroRouter(options?) |
Inject store. Pass { root: true } for root in nested setups. |
useMicroRouter() |
Auto-typed via Register augmentation. Explicit <T> generic also supported. |
useMicroState<T>(defaults?) |
Reactive attrs bridge — read/write props in routes, dialogs, controls. |
useRouteLifecycle(hooks) |
onRouteEnter / onRouteLeave — fires when page becomes/stops being top. |
useDialogLifecycle(hooks) |
onDialogEnter / onDialogLeave — fires when dialog becomes/stops being topmost. |
useControlLifecycle(hooks) |
onControlEnter / onControlLeave — fires when control activates/deactivates. |
usePageTracker(hooks?) |
Normalize tracker hooks with no-op fallbacks. |
useNavigation(config?, tracker?) |
Low-level page navigation (used internally). |
useDialogManager(tracker?) |
Low-level dialog management (used internally). |
useControlManager(config?, tracker?) |
Low-level control management (used internally). |
useGestureNavigation(config, ctx) |
Swipe-back gesture handler (used internally by MicroRouterView). |
Components
| Component | Description |
|---|---|
<MicroRouterView> |
Root wrapper — renders pages, dialogs, controls. Accepts nested prop. |
<RoutePage> |
Page slot wrapper — provides attrs injection. |
<MicroDialogComponent> |
Headless native <dialog> — focus trap, backdrop, escape key. |
<MicroControlWrapper> |
Control slot wrapper — provides attrs injection. |
Plugin Helpers
| Function | Description |
|---|---|
defineFeaturePlugin(config) |
Create a feature plugin bundle. |
registerFeaturePlugins(plugins, store) |
Register all plugins with the store. |
Types
| Type | Description |
|---|---|
NavigationGuard |
(to, from) => boolean | Promise<boolean> |
NavigationAfterHook |
(to, from) => void |
RouteMap |
Record<string, Record<string, unknown> | undefined> |
TypedPush<T> |
Type-safe push overloads for a RouteMap |
SerializedState |
JSON-serializable router state snapshot |
AudioAdapter |
Abstract audio playback interface |
GestureConfig |
Gesture navigation configuration |
Performance
| Metric | Value |
|---|---|
| Core bundle (gzip) | 10.35 kB |
| Audio (gzip) | 1.07 kB |
| Styles (gzip) | 0.84 kB |
| Navigation latency (p50) | < 0.01 ms |
| Tests | 150 passing |
| Coverage | ~96% |
Run benchmarks: bun run bench (navigation timing) / bun run bench:size (bundle check)
Peer Dependencies
vue>= 3.4.0howler>= 2.2.0 (optional — only forvue-micro-router/audio)@vue/devtools-api>= 6.0.0 (optional — only for devtools inspector)
License
MIT
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.1.2 | ^3.4.0 | MIT | |
| 1.0.63 | ^3.4.0 | MIT | |
| 1.0.62 | ^3.4.0 | MIT | |
| 1.0.61 | ^3.4.0 | MIT | |
| 1.0.60 | ^3.4.0 | MIT | |
| 1.0.59 | ^3.4.0 | MIT | |
| 1.0.58 | ^3.4.0 | MIT | |
| 1.0.57 | ^3.4.0 | MIT | |
| 1.0.56 | ^3.4.0 | MIT | |
| 1.0.55 | ^3.4.0 | MIT | |
| 1.0.53 | ^3.4.0 | MIT | |
| 1.0.52 | ^3.4.0 | MIT | |
| 1.0.51 | ^3.4.0 | MIT | |
| 1.0.50 | ^3.4.0 | MIT | |
| 1.0.49 | ^3.4.0 | MIT | |
| 1.0.48 | ^3.4.0 | MIT | |
| 1.0.47 | ^3.4.0 | MIT | |
| 1.0.46 | ^3.4.0 | MIT | |
| 1.0.45 | ^3.4.0 | MIT | |
| 1.0.44 | ^3.4.0 | MIT | |
| 1.0.43 | ^3.4.0 | MIT | |
| 1.0.42 | ^3.4.0 | MIT | |
| 1.0.41 | ^3.4.0 | MIT | |
| 1.0.40 | ^3.4.0 | MIT | |
| 1.0.39 | ^3.4.0 | MIT | |
| 1.0.38 | ^3.4.0 | MIT | |
| 1.0.37 | ^3.4.0 | MIT | |
| 1.0.36 | ^3.4.0 | MIT | |
| 1.0.34 | ^3.4.0 | MIT | |
| 1.0.33 | ^3.4.0 | MIT |
Showing the latest 30 of 56. See every release on npm