# Native Vue Router usage guide Native Vue Router adds gesture-driven, interruptible navigation to Vue 3 while leaving Vue Router responsible for route matching, URLs, redirects, guards, and browser history. It renders a provisional destination beside the current route, lets a pointer gesture control the transition, and commits the Vue Router navigation only when the gesture completes. The current support contract is Vue 3.5+, Vue Router 5, and client-side DOM rendering. SSR hydration of a live native view stack is not yet a complete feature. ## Packages | Package | Purpose | | ---------------------------------- | ------------------------------------------------------------------------ | | `@native-vue-router/core` | Runtime, route surfaces, gestures, caching, lifecycle APIs, and profiler | | `@native-vue-router/preset-native` | Native-looking back and tab controls, safe-area CSS, and motion tokens | | `@native-vue-router/capacitor` | Hardware Back, deep links, app lifecycle, root exit, and haptics | | `@native-vue-router/electron` | Host back/forward integration and Chromium history-gesture suppression | ## Install Install the core package alongside its peer dependencies: ```bash npm install vue@^3.5 vue-router@^5 @native-vue-router/core ``` Add optional packages only when the application uses them: ```bash npm install @native-vue-router/preset-native npm install @native-vue-router/capacitor @capacitor/app @capacitor/core @capacitor/haptics npm install @native-vue-router/electron ``` When consuming this repository directly, build the packages first and install the required package directories or packed tarballs into the target project: ```bash npm install npm run build:packages npm pack --workspace @native-vue-router/core ``` Import the core stylesheet once from the application entry point. Import the preset stylesheet as well when using its controls: ```ts import "@native-vue-router/core/style.css"; import "@native-vue-router/preset-native/style.css"; // optional ``` The elements containing the navigator must have a definite height. A typical full-screen application uses: ```css html, body, #app { width: 100%; height: 100%; margin: 0; } ``` ## Minimal setup Create the Vue Router first, then create and install the native runtime. Install Vue Router before Native Vue Router so route injection and Options API `$route` scoping are configured correctly. ```ts // src/main.ts import { createApp } from "vue"; import { createRouter, createWebHistory } from "vue-router"; import { createNativeRouter } from "@native-vue-router/core"; import App from "./App.vue"; import HomeView from "./views/HomeView.vue"; import ProductView from "./views/ProductView.vue"; import "@native-vue-router/core/style.css"; export const router = createRouter({ history: createWebHistory(), routes: [ { path: "/", name: "home", component: HomeView }, { path: "/products/:id", name: "product", component: ProductView, meta: { native: { presentation: "push", parent: "/", gesture: "edge", }, }, }, ], }); export const nativeRouter = createNativeRouter({ router, cache: { maxInactive: 4 }, }); const app = createApp(App); app.use(router); app.use(nativeRouter); await router.isReady(); app.mount("#app"); ``` Replace the application-level `` with `` and wrap the navigation surface in `` to enable predictive Back. ```vue ``` Nested `` components inside route components continue to work. Use one application-level `NativeRouterView` for one native runtime; independent nested native navigators are not currently a complete feature. ## Route metadata Declare presentation, topology, history, cache, and gesture policy next to each route: ```ts import type { RouteRecordRaw } from "vue-router"; export const routes: RouteRecordRaw[] = [ { path: "/inbox", name: "inbox", component: () => import("./views/InboxView.vue"), meta: { native: { siblingGroup: "primary", siblingOrder: 0, siblingHistory: "replace", cache: "pin", gesture: "full", }, }, }, { path: "/chat/:id", name: "chat", component: () => import("./views/ChatView.vue"), meta: { native: { presentation: "push", parent: "/inbox", gesture: "edge", }, }, }, { path: "/chat/:id/details", name: "chat-details", component: () => import("./views/ChatDetailsView.vue"), meta: { native: { parent: (route) => ({ name: "chat", params: { id: route.params.id }, query: route.query, }), }, }, }, ]; ``` | Option | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `presentation` | `push`, `reveal`, `slide`, `fade`, `modal`, `sheet`, `none`, or a registered custom name | | `transition` | Compatibility alias for `presentation`; prefer `presentation` in new code | | `parent` | Logical Back target for a cold-start/deep-linked route; may be a location or a function of the current route | | `siblingOrder` | Numeric visual order used to derive sibling direction | | `siblingHistory` | `replace` keeps peer selections out of Back history; `push` makes them Back destinations | | `cache` | `false` unmounts when inactive, `true` uses normal retention, and `pin` exempts the view from ordinary LRU trimming | | `gesture` | `false` disables navigator gestures for the route; `edge` and `full` describe intended policy | | `navigator`, `siblingGroup` | Reserved topology labels; the current navigator still receives its sibling list explicitly | Declare `parent` for detail, settings, modal, and other routes that should have a predictive destination when opened directly. It is a logical product relationship, not proof that a matching browser history entry exists. ## Navigate Use `useNativeRouter()` inside `setup()`: ```vue ``` The same runtime is available as `this.$nativeRouter` in Options API components. | Method | Use | | ---------------------------- | --------------------------------------------------------------------- | | `push(to, options?)` | Add an animated history entry | | `replace(to, options?)` | Replace the current history entry | | `sibling(to, options?)` | Move between ordered peer routes | | `pop()` | Navigate to the previous native history entry or declared parent | | `present(to, presentation?)` | Present a route, using `modal` by default | | `dismiss()` | Return from a presented route | | `preload(to)` | Resolve and load a lazy route without mounting or committing it | | `unload(to)` | Unmount inactive instances of one location and return the count | | `trimCache(options?)` | Unmount inactive cached views while retaining lightweight descriptors | Navigation options can override `presentation`, `replace`, `direction`, and `sourceRect`. The navigation methods return `true` when Vue Router accepts the commit and `false` for a no-op, cancellation, or rejected navigation. Use normal `router.push()` for redirects or flows that intentionally do not need a native preview. The runtime reconciles external Vue Router navigations, but they do not receive the same preview-driven transition. ## Links and gesture components ### NativeLink `NativeLink` renders a real anchor, resolves its `href`, preserves modified-click behavior, and routes an ordinary primary click through the native runtime. ```vue ``` Its navigation props are `to`, `replace`, and `presentation`; other attributes are passed to the anchor. ### NativeGestureLink `NativeGestureLink` lets a horizontal drag on a component reveal its destination. It accepts `to`, `presentation` (default `reveal`), `replace`, `direction` (`left`, `right`, or `any`), and `as` (default `div`). ```vue ``` Choose a semantic `as` element and provide keyboard behavior when the result is interactive. A normal click also invokes native `push()`. ### NativeNavigator Pass ordered peer locations to enable full-surface horizontal sibling paging. The leading edge remains reserved for Back when `canGoBack` is true. ```vue ``` Inputs, editable content, links, buttons, and elements carrying `data-native-gesture="ignore"` are excluded from automatic gesture recognition. Use the explicit attribute for carousels, maps, editors, canvases, or other regions that own horizontal input. ### NativeDismissGesture Wrap a custom full-height modal surface to make a downward drag dismiss it: ```vue ``` ### NativeSheet Use `NativeSheet` inside a route whose presentation is `sheet`. It keeps the surface below the device's safe top inset, leaves the previous route visible but inert beneath a backdrop, and includes a drag handle. With no breakpoints, the surface grows to its content and is capped at the available device height: ```vue ``` Supply fractional breakpoints to create snap points. Fractions are measured against the route height after the safe top inset has been reserved: ```vue ``` Users drag the handle or use Arrow Up/Down, Home, and End while it is focused. Dragging below the smallest point dismisses the route. The backdrop and Escape also dismiss by default. The scrollable sheet body participates in the same gesture automatically. At an interior scroll position, the content scrolls normally. When the content is at the top, pulling down hands the gesture to the sheet so it can move to a lower point or dismiss. When the content is at the bottom, pushing upward grows the sheet to its next point. Mouse/pen dragging, touch input, and thresholded trackpad/wheel overscroll follow the same boundary rules. Gesture ownership is chosen from the initial directional intent and remains locked until release. A gesture that starts while content exists in that direction stays a content gesture even if it reaches an edge or reverses. Lift and begin a new gesture at the edge to resize the sheet. This prevents content and sheet movement from overlapping and prevents previously scrolled distance from becoming a sheet-height jump. | Prop | Meaning | | ------------------- | --------------------------------------------------------------------------------------- | | `breakpoints` | Unique fractions greater than `0` and at most `1`; an empty list enables content height | | `initialBreakpoint` | Initial fraction, snapped to the nearest declared point | | `modelValue` | Current fractional point for `v-model` | | `dismissible` | Enables sheet-triggered drag and keyboard dismissal; default `true` | | `backdropDismiss` | Lets a backdrop click dismiss; default `true` | | `showHandle` | Renders the built-in drag/keyboard handle; default `true` | | `ariaLabel` | Accessible dialog label; default `Sheet` | Use the `handle` slot to replace the visual handle without replacing its input behavior. Theme the surface with `--nvr-sheet-background`, `--nvr-sheet-backdrop`, `--nvr-sheet-radius`, and `--nvr-sheet-top-gap`. For a route opened with `native.present(to, "sheet")`, the runtime remembers the sheet presentation after commit. Defining `meta.native.presentation: "sheet"` as well makes direct URL entry and raw Vue Router navigation use the same contained layout. During presentation and dismissal, `NativeSheet` moves its actual surface by that surface's height rather than translating a transparent viewport-sized route wrapper. The source route remains continuously scaled as the sheet's underlay, avoiding a geometry jump when the route transaction commits. ## Tabs and back controls The optional native preset supplies a Back button and tab bar: ```vue ``` The preset tab bar uses replace-style sibling navigation. Build a product-specific control with `native.sibling()` when tabs need different history semantics. ## Route params, query, hash, and injected route state Pass any normal `RouteLocationRaw` to native navigation methods and components. `useRoute()`, `useRouter()`, and Options API `this.$route` work within active and preview route trees. During a held gesture, the destination subtree sees its own params, query, hash, matched records, and metadata even though `router.currentRoute` still points at the committed source route. This distinction is intentional: - Read `useRoute()` or `$route` inside a route component for that surface's scoped route. - Read `router.currentRoute` only when the application needs the globally committed route. - Expect the two values to differ while a preview is visible. Normal Vue `provide()` and `inject()` work across the route surface. App-level provides, plugin provides, and values provided by route components remain available to descendants. ## Lifecycle and cached views Native Vue Router can keep inactive route component trees mounted. Vue's normal mount, update, and unmount hooks continue to describe component lifetime, but being mounted does not mean the route is the current screen. Use the native lifecycle for route visibility and activity: ```vue ``` `isActive` means Vue Router has made the route authoritative. `isVisible` is also true for either side of an interactive transition and for a sheet's visual underlay. An underlay remains inert and is not active. `isCached` identifies a mounted inactive view; `isPreview` identifies an uncommitted destination. Use active effects for polling, subscriptions, media, analytics, and work that should run only on the current route. Use visible effects for rendering work needed while the route is on screen during a transition. Put durable state in a store or persistence layer because cache eviction unmounts component-local state. The default cache limit is four inactive, non-pinned views. History descriptors remain after a component tree is evicted and are remounted if navigation reaches them later. ## Vue built-in components `NativeRouterView` exposes `Component`, `route`, and `entry` through its default slot, so normal Vue wrappers can be used inside each native route layer: ```vue ``` These components retain their normal Vue meaning: - `` caches components selected within that route layer. It is not a replacement for the native multi-route cache, and native route changes alone do not imply Vue `onActivated()` or `onDeactivated()`. - `` animates changes inside a layer. Native presentations animate the route layers themselves. - `` may show a fallback while an async preview component resolves. - `` can move DOM outside the layer. Because teleported DOM is outside the layer's `inert` and `aria-hidden` boundary, close or hide overlays whenever the owning native view is not visible. A visibility-safe teleported overlay looks like this: ```vue ``` Options API lifecycle hooks (`beforeCreate`, `created`, `beforeMount`, `mounted`, `beforeUpdate`, `updated`, `beforeUnmount`, and `unmounted`) and the corresponding Composition API hooks keep their standard Vue behavior. ## Guards, redirects, and preview side effects Forward navigation resolves and loads the route component before commit so the user can drag a live destination. Vue Router guards run when the runtime commits the real `push()`, `replace()`, or Back operation. A rejected guard removes the preview and restores the source route; a redirect is reconciled to the route Vue Router accepts. Consequently, a preview component may execute `setup()` and mount before a guard allows entry, then unmount without ever becoming active. Avoid irreversible work such as analytics events, mutations, purchases, or permanent subscriptions in unconditional setup/mount code. Tie committed-screen behavior to `useNativeViewActiveEffect()` or an explicit committed application action. ## Custom presentations Register a presentation at runtime or pass it in `createNativeRouter()`: ```ts import { createNativeRouter, definePresentation, } from "@native-vue-router/core"; const scaleFade = definePresentation({ name: "scale-fade", axis: "x", layerStyle({ role, progress }) { return role === "to" ? { opacity: progress, transform: `scale(${0.94 + progress * 0.06})`, } : { opacity: 1 - progress * 0.25 }; }, }); const nativeRouter = createNativeRouter({ router, presentations: [scaleFade], }); // This is also valid later: nativeRouter.registerPresentation(scaleFade); ``` Reference the registered name from route metadata or a navigation option. Presentation functions should derive compositor-friendly styles from progress; they must not mutate history or application state. Advanced interactions can call `beginInteractive()`, `updateInteractive()`, `finishInteractive()`, and `cancelInteractive()` directly. Progress is normalized from `0` to `1`, and velocity is normalized route progress per second. Cancel the transaction when the owning component unmounts and ignore stale async results by checking the returned transaction ID. ## Platform adapters ### Capacitor ```ts import { createCapacitorAdapter } from "@native-vue-router/capacitor"; const nativeRouter = createNativeRouter({ router, platform: createCapacitorAdapter({ haptics: true, exitAtRoot: true, trimCacheOnPause: true, deepLinkPath: (url) => `${url.pathname}${url.search}${url.hash}`, }), }); ``` The adapter handles hardware Back, launch/app URLs, pause cancellation, cache trimming, optional haptics, and optional exit at the root. Configure Universal Links/App Links in the native project separately. ### Electron Disable Chromium's competing overscroll navigation in the main process before `app.whenReady()`: ```ts import { app } from "electron"; import { disableElectronHistoryGestures } from "@native-vue-router/electron"; disableElectronHistoryGestures(app.commandLine); ``` Install the renderer adapter after exposing the documented `window.nativeVueHost` back/forward/memory-pressure bridge from a secure preload: ```ts import { createElectronRendererAdapter } from "@native-vue-router/electron"; const nativeRouter = createNativeRouter({ router, platform: createElectronRendererAdapter(), }); ``` Use hash history for packaged `file:` applications unless the Electron host serves navigation URLs through an application protocol. ### Browser and PWA The core works in normal browser tabs, but a browser may reserve an edge gesture before page JavaScript can claim it. An installed iOS PWA can improve gesture ownership with an early non-passive edge guard, but web content cannot change `WKWebView.allowsBackForwardNavigationGestures`. Use Capacitor when deterministic native-level ownership is required. ## Add to an existing Vue Router application Adopt the library incrementally: 1. Confirm Vue 3.5+ and Vue Router 5, then install core and its CSS. 2. Create the native runtime from the existing router and install it after `app.use(router)`. 3. Replace only the root visual outlet with `NativeRouterView`; leave nested router views in route components intact. 4. Change navigation that needs native motion from `router.push()`/`RouterLink` to runtime methods or `NativeLink`. Keep ordinary Vue Router calls where no native transition is wanted. 5. Add `parent` metadata to deep-linkable child routes. 6. Identify peer routes such as tabs, assign `siblingOrder`, choose `siblingHistory`, and pass their locations to `NativeNavigator`. 7. Move active-screen side effects from unconditional mount hooks into native active or visible effects. 8. Mark nested horizontal controls with `data-native-gesture="ignore"` and add component gesture links only where the product intends them. 9. Choose cache policy per route and move durable state out of component-local memory. 10. Exercise URLs, redirects, rejected guards, direct deep links, browser Back and Forward, held/cancelled gestures, reduced motion, and keyboard/focus behavior before broad rollout. For a lower-risk migration, start with button-driven `push()`, `pop()`, and `present()`. Add predictive Back, siblings, and component-originated gestures after the route topology and lifecycle behavior are verified. ## Diagnostics and profiling Inspect the runtime's reactive state while integrating: ```ts const native = useNativeRouter(); watchEffect(() => { console.table(native.cacheStats.value); console.log(native.transaction.value); }); const stop = native.onDiagnostic((event) => { console.debug("native-navigation", event); }); ``` The opt-in profiler records frame cadence and timing-safe navigation events: ```ts import { createNativeNavigationProfiler } from "@native-vue-router/core"; const profiler = createNativeNavigationProfiler(nativeRouter, { metadata: { build: import.meta.env.VITE_BUILD_ID }, }); profiler.start(); // Reproduce the navigation. const report = profiler.stop(); const json = profiler.toJSON(report); profiler.dispose(); ``` Route params, query values, and application state are omitted from profiler route labels. ## Troubleshooting **The route surface is blank or has zero height.** Give `html`, `body`, `#app`, and the application shell a definite height, and import the core stylesheet. **`useNativeRouter()` says the plugin is not installed.** Create one runtime and call `app.use(nativeRouter)` before mounting the app. **`useNativeViewLifecycle()` throws.** Call it only from a component rendered inside `NativeRouterView`. **A route mounts even though a guard rejects it.** This is preview behavior, not a committed navigation. Move irreversible work to an active effect. **Back has no visual destination after a direct deep link.** Add a `parent` location or parent function to that route's native metadata. **A tab appears in browser Back history.** Set `siblingHistory: "replace"` and use `native.sibling()` or the preset tab bar. **A carousel or editor fights the route gesture.** Put `data-native-gesture="ignore"` on the region that owns the input. **A teleported dialog remains interactive from an inactive cached route.** Gate the teleport content on `useNativeViewLifecycle().isVisible` and close it on hide/deactivate when appropriate. **Local state disappears.** The cache is bounded and can be trimmed by a host. Use `cache: "pin"` sparingly or store durable state outside the route component. ## Further reading - [How the preview and commit model works](docs/how-it-works.md) - [Architecture reference](docs/architecture.md) - [Engineering constraints and trade-offs](docs/challenges-and-tradeoffs.md) - [Scalability and extension points](docs/principles-and-scalability.md) - [Platform integration](docs/platforms.md) - [Interactive demo](apps/demo) ## AI agent skill This repository includes a portable integration skill at [`skills/integrate-native-vue-router`](skills/integrate-native-vue-router). Copy that complete directory into the skills location recognized by the agent (for Codex, normally `~/.codex/skills/`) and invoke it as `$integrate-native-vue-router`. Keep `SKILL.md`, `agents/openai.yaml`, and the `references` directory together so the integration workflow retains its API and verification reference.