Files
Native-Router-Vue/skills/integrate-native-vue-router/references/integration-reference.md

16 KiB

Native Vue Router integration reference

Use this reference for @native-vue-router/* version 0.1.x. Inspect installed declarations or local source when integrating another version.

Support and ownership

  • Requires Vue ^3.5.0 and Vue Router ^5.0.0.
  • Targets client-side DOM navigation. SSR/live-stack hydration is incomplete.
  • Vue Router owns committed routes, URLs, history, guards, redirects, and lazy route matching.
  • Native Vue Router owns preview surfaces, mounted-view caching, gesture progress, presentations, and transaction state.
  • Preview trees receive scoped Vue Router route injection. useRoute() and Options API $route identify that surface; router.currentRoute identifies the globally committed route.
  • The URL does not change during a forward preview. Vue Router navigation and guards run when the gesture or imperative transaction commits.

Packages and styles

npm install vue@^3.5 vue-router@^5 @native-vue-router/core

Optional packages:

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

Import styles explicitly:

import "@native-vue-router/core/style.css";
import "@native-vue-router/preset-native/style.css"; // when used

Give the route shell a definite height. The core .nvr-navigator and .nvr-router-view elements use height: 100%.

Bootstrap template

import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { createNativeRouter } from "@native-vue-router/core";
import App from "./App.vue";
import "@native-vue-router/core/style.css";

const router = createRouter({
  history: createWebHistory(),
  routes,
});

const nativeRouter = createNativeRouter({
  router,
  cache: { maxInactive: 4 },
  // edgeWidth: 28,
  // platform,
  // presentations: [customPresentation],
});

const app = createApp(App);
app.use(router);
app.use(nativeRouter);
await router.isReady();
app.mount("#app");
<script setup lang="ts">
import { NativeNavigator, NativeRouterView } from "@native-vue-router/core";

const siblings = ["/inbox", "/stories", "/profile"];
</script>

<template>
  <NativeNavigator :siblings="siblings" :edge-width="28">
    <NativeRouterView />
  </NativeNavigator>
</template>

Install Vue Router before the native plugin. Retain ordinary nested RouterViews within route components.

Route metadata

interface NativeRouteOptions {
  navigator?: string;
  presentation?:
    "push" | "reveal" | "slide" | "fade" | "modal" | "sheet" | "none" | string;
  transition?: string;
  parent?: RouteLocationRaw | ((route) => RouteLocationRaw);
  siblingGroup?: string;
  siblingOrder?: number;
  siblingHistory?: "push" | "replace";
  cache?: boolean | "pin";
  gesture?: boolean | "edge" | "full";
}
  • Prefer presentation; transition is a compatibility alias.
  • parent supplies a synthetic predictive Back destination if no warm native history predecessor exists.
  • siblingOrder determines direction for programmatic sibling navigation.
  • siblingHistory: "replace" is the usual tab behavior.
  • cache: false prevents inactive retention. cache: "pin" avoids ordinary LRU trimming and is excluded from default trimCache().
  • gesture: false is enforced by NativeNavigator. In 0.1.x, finer edge/full policy is mainly structural.
  • navigator and siblingGroup are reserved; pass siblings to the navigator explicitly.

Example with dynamic parent state:

{
  path: "/chat/:id/details",
  name: "chat-details",
  component: () => import("./ChatDetailsView.vue"),
  meta: {
    native: {
      presentation: "push",
      parent: (route) => ({
        name: "chat",
        params: { id: route.params.id },
        query: route.query,
        hash: route.hash,
      }),
      gesture: "edge",
    },
  },
}

Core components

NativeRouterView

Renders every mounted native entry as a sibling layer. Its default slot exposes:

{
  Component: Component | undefined;
  route: RouteLocationNormalizedLoaded;
  entry: NativeViewEntry;
}

Without a slot, it renders the matched route component. Inactive layers are hidden, inert, and aria-hidden.

NativeNavigator

Props:

  • siblings: RouteLocationRaw[] (default [])
  • edgeWidth: number (default 28)

Owns leading-edge predictive Back and optional full-surface paging between the listed siblings. The list is visual order. Route siblingOrder also makes programmatic direction deterministic.

Props:

  • to: RouteLocationRaw (required)
  • replace: boolean
  • presentation: NativePresentationName

Renders an anchor with a resolved href. Primary unmodified clicks use native push or replace; other attributes pass through.

Props:

  • to: RouteLocationRaw (required)
  • presentation (default "reveal")
  • replace: boolean
  • direction: "left" | "right" | "any" (default "any")
  • as: string (default "div")

Owns horizontal component-originated navigation. A click uses native push. Choose semantic markup and add accessible keyboard behavior as needed.

NativeDismissGesture

Prop as defaults to div. Owns downward vertical dismissal for a modal or sheet surface.

NativeSheet

Use inside a route with meta.native.presentation: "sheet". The sheet route is contained below --nvr-safe-top plus --nvr-sheet-top-gap, and its previous route remains visible but inert as an underlay.

Props:

  • breakpoints: number[] (default []): normalized fractions in (0, 1]. Empty means content height capped at available height.
  • initialBreakpoint?: number: starts at the nearest declared fraction.
  • modelValue?: number: current fraction for v-model.
  • dismissible: boolean (default true).
  • backdropDismiss: boolean (default true).
  • showHandle: boolean (default true).
  • ariaLabel: string (default "Sheet").

Events are update:modelValue, breakpoint-change, and dismiss. The named handle slot replaces only the handle visual. The built-in handle supports pointer dragging, Escape, Arrow Up/Down, Home, and End.

The body keeps native scrolling while it has content in the requested direction. At the top, downward overscroll shrinks or dismisses the sheet. At the bottom, upward overscroll expands it. The handoff supports touch, mouse/pen dragging, and thresholded wheel/trackpad input. A snapping NativeSheet animates its own measured surface, while the route-sized wrapper stays fixed and the previous route retains its underlay scale across commit.

Ownership locks after the gesture's initial directional intent. If content owns the gesture, reaching an edge or reversing does not transfer that same gesture to the sheet; release and start at the edge to resize. Never implement a mid-gesture reinterpretation using distance accumulated while content was scrolling.

<script setup lang="ts">
import { ref } from "vue";
import { NativeSheet } from "@native-vue-router/core";

const point = ref(0.55);
</script>

<template>
  <NativeSheet
    v-model="point"
    :breakpoints="[0.3, 0.55, 1]"
    :initial-breakpoint="0.55"
    aria-label="Filters"
  >
    <FilterForm />
  </NativeSheet>
</template>

Theme with --nvr-sheet-background, --nvr-sheet-backdrop, --nvr-sheet-radius, and --nvr-sheet-top-gap. Prefer NativeSheet to NativeDismissGesture for partial or snapping route sheets.

Gesture exclusions

The recognizers ignore ordinary form/editable controls and elements within:

<div data-native-gesture="ignore">...</div>

Use this for maps, carousels, canvases, code editors, and custom horizontal controls.

Runtime API

Create/inject:

  • createNativeRouter(options): NativeRouterRuntime
  • useNativeRouter(): NativeRouterRuntime
  • Options API: this.$nativeRouter

Reactive readonly state:

  • router
  • entries
  • activeKey
  • transaction
  • canGoBack
  • cacheStats

Navigation:

push(to, options?): Promise<boolean>
replace(to, options?): Promise<boolean>
sibling(to, options?): Promise<boolean>
pop(): Promise<boolean>
present(to, presentation = "modal"): Promise<boolean>
dismiss(): Promise<boolean>
preload(to): Promise<RouteLocationNormalizedLoaded>

NativeNavigationOptions supports:

{
  presentation?: NativePresentationName;
  replace?: boolean;
  direction?: "forward" | "back" | "up" | "down";
  sourceRect?: {
    top: number;
    left: number;
    width: number;
    height: number;
    viewportWidth: number;
    viewportHeight: number;
  };
}

Interactive driver:

beginInteractive(kind, to?, options?): Promise<number | null>
updateInteractive(progress, velocity?): void
finishInteractive(forceCommit?): Promise<boolean>
cancelInteractive(): Promise<void>

Kinds are push, pop, sibling, present, and dismiss. Progress is clamped to 0...1. Velocity is normalized route progress per second. The default commit rule is progress at least 0.36, or progress at least 0.08 with velocity at least 1.1.

Cache/diagnostics/extension:

unload(to): number
trimCache({ includePinned?, reason? }?): void
onDiagnostic(listener): () => void
registerPresentation(definition): void
presentationFor(name): NativePresentationDefinition | undefined
dispose(): void

Call dispose() if the owning application lifecycle tears the runtime down without a page unload.

Native view lifecycle

Available APIs:

useNativeViewLifecycle();
onNativeViewActivate(hook);
onNativeViewDeactivate(hook);
onNativeViewShow(hook);
onNativeViewHide(hook);
onNativeViewEvict(hook);
useNativeViewActiveEffect(effect);
useNativeViewVisibleEffect(effect);

Lifecycle state:

  • route: scoped route for the view.
  • status: active, inactive, preview, or evicted.
  • role: active, inactive, underlay, from, or to.
  • isActive: authoritative committed route.
  • isVisible: active, one side of a live transition, or a visible sheet underlay. Underlays remain inert.
  • isPreview: destination not yet committed.
  • isCached: mounted inactive route.
  • evictionReason: cache-disabled, cache-limit, navigation-rejected, popped, manual, trimmed, or memory-pressure.

Effects may return cleanup functions. Active effects suit polling, subscriptions, media, and committed-screen analytics. Visible effects suit work needed while the surface participates in a transition.

Do not treat Vue component mount as route commitment. A preview can mount before guards run and unmount after rejection or gesture cancellation.

Vue compatibility

Within a native route surface:

  • Vue Composition and Options API lifecycle hooks retain normal component semantics.
  • App/plugin/ancestor provide and inject work normally.
  • useRoute, useRouter, and Options API $route are supported.
  • Params, query, hash, metadata, and matched records are scoped to each preview or committed surface.
  • Nested RouterView is supported.
  • KeepAlive, Transition, Teleport, and Suspense may be used inside the NativeRouterView slot.

Important distinctions:

  • Native route caching is separate from KeepAlive. A cached native route stays mounted, so native inactivity alone does not trigger Vue onDeactivated.
  • Native presentations animate route layers; a Vue Transition handles changes within one layer.
  • Teleported DOM escapes the route layer's inert/aria-hidden isolation. Gate it with isVisible and close it when the owning view hides where appropriate.
  • Async route components are loaded before preview. A Suspense inside the route can still show fallback UI for async descendants.

Slot composition example:

<NativeRouterView v-slot="{ Component, route }">
  <Suspense>
    <Transition name="content" mode="out-in">
      <KeepAlive :max="3">
        <component :is="Component" :key="route.fullPath" />
      </KeepAlive>
    </Transition>
    <template #fallback><RouteSkeleton /></template>
  </Suspense>
</NativeRouterView>

Custom presentation

import { definePresentation } from "@native-vue-router/core";

const scaleFade = definePresentation({
  name: "scale-fade",
  axis: "x",
  layerStyle({ role, progress, direction, sourceRect }) {
    return role === "to"
      ? { opacity: progress, transform: `scale(${0.94 + progress * 0.06})` }
      : { opacity: 1 - progress * 0.25 };
  },
});

Register through createNativeRouter({ presentations: [...] }) or runtime.registerPresentation(). Keep layer styles compositor-friendly and do not perform application or history mutations in a style function.

Preset-native API

  • NativeBackButton prop: label (default "Back"). Calls native.pop().
  • NativeTabBar prop: required items: NativeTabItem[]. It uses replace-style native sibling navigation.
  • NativeTabItem: { label, to, icon?, activeWhen? }.
  • detectNativePlatform(): "ios" | "android" | "desktop".
  • nativeMotionTokens: edge widths and commit thresholds for those three platform labels. Core physics are not route-configurable from these tokens in 0.1.x.

Capacitor adapter

createCapacitorAdapter({
  exitAtRoot?: boolean;          // default true behavior
  haptics?: boolean;             // default enabled on native
  trimCacheOnPause?: boolean;    // default true
  deepLinkPath?: (url: URL) => string;
})

It handles Android hardware Back, App URL open/launch URLs, interactive cancellation on pause, optional inactive-cache trimming, root exit, and haptics. Native link association/configuration remains an application responsibility.

Electron adapter

Main process, before app.whenReady():

disableElectronHistoryGestures(app.commandLine);

Renderer:

createNativeRouter({
  router,
  platform: createElectronRendererAdapter(),
});

The preload may expose:

window.nativeVueHost = {
  onBack(callback): () => void,
  onForward?(callback): () => void,
  onMemoryPressure?(callback): () => void,
};

Use a secure context bridge; do not enable Node integration merely for the adapter. Packaged file: apps commonly require hash history.

Verification matrix

At minimum verify:

Area Checks
Boot Initial route renders after router.isReady; CSS and height are correct
Route state Params, query, hash, useRoute, $route, nested routes, provide/inject
Preview Source and destination coexist; URL stays on source; cancellation removes preview
Guards Acceptance, rejection, and redirect reconcile to Vue Router's result
Back Warm Back, browser Back/Forward, direct-entry parent, no unrelated cached target
Siblings Direction follows order; chosen push/replace history behavior is correct
Cache Local warm state, cache: false, pin, LRU, unload, trim, eviction cleanup
Lifecycle Active/visible effects stop and resume at the correct boundaries
Vue KeepAlive, Transition, Teleport, Suspense, component hook ordering
Input Edge Back, component drag, vertical scroll, ignored regions, rapid interruption
A11y Inactive focus isolation, semantic controls, keyboard access, reduced motion
Hosts PWA limits, Electron commands/history, Capacitor Back/deep links/pause

Known boundaries

  • One runtime coordinates one authoritative visual transaction at a time.
  • Preview setup can run before navigation approval.
  • The mounted cache is bounded; route-local state can be lost after eviction.
  • Cold-start predictive Back requires explicit parent topology.
  • Browser content cannot guarantee ownership of operating-system/browser edge gestures.
  • Independent nested navigation controllers and SSR stack hydration need an explicit design beyond the current 0.1.x implementation.