12 KiB
How Native Vue Router Works
Purpose
Native Vue Router adds an interactive visual navigation layer to Vue Router. Its goal is not merely to make route changes slide instead of fade. It is designed so navigation itself can be manipulated: a user can reveal a destination, stop halfway, reverse direction, release with velocity, or begin another navigation before the previous animation has settled.
The library supports four related navigation styles through one transaction engine:
- Edge-driven predictive back, including drag-and-hold.
- One-to-one paging between ordered sibling routes such as primary tabs.
- Component-originated navigation, where dragging a row or card reveals its destination.
- Presented routes such as modals and sheets, including interactive dismissal.
Buttons and links use the same engine as gestures. A tab click, back button, programmatic push, and interactive swipe differ only in how progress is supplied and whether the final commit decision is forced.
The central idea: semantic state and visual state are different
Vue Router remains the authority for semantic navigation: route matching, URLs, parameters, redirects, guards, lazy route modules, and browser history. Native Vue Router owns temporary visual state: mounted route surfaces, interactive progress, motion, and the relationship between the surface being left and the surface being revealed.
That separation produces two ledgers:
| Ledger | Owns | Source of truth for |
|---|---|---|
| Vue Router | Current committed route and browser history | What URL the application is actually on |
| Native view runtime | Active, inactive, preview, and evicted view entries | What route surfaces can be rendered during motion |
The ledgers agree at rest. During a gesture they intentionally diverge: Vue Router still reports the committed from route while the native runtime also renders an uncommitted to route.
Preview before commit
A forward interaction follows a two-phase process:
- Resolve the target with Vue Router.
- Load its lazy route component without pushing a history entry.
- Add a preview entry to the native view ledger.
- Render both the committed route and preview route through explicit
<RouterView :route>instances. - Drive their transforms from normalized gesture progress between
0and1. - On release, decide whether to commit from distance and velocity.
- Only then call Vue Router's
push(),replace(), or browser-backedback(). - Reconcile the visual ledger with the route Vue Router actually accepted.
If the user reverses the gesture or a navigation guard rejects the target, the preview springs away and is removed. The URL never briefly changes to a route that the user did not commit.
Each preview subtree receives its own scoped Vue Router route injection. As a result, components rendered in the preview see the preview's params and metadata through useRoute(), even though the application's global committed route has not changed yet.
Predictive back
Back navigation is harder than forward navigation because browser history does not expose a reliable, portable list of previous route objects. The runtime therefore maintains its own committed history-key ledger alongside Vue Router.
When a back gesture starts, the runtime locates the preceding native view entry and renders it beneath the current one. The browser URL remains unchanged while the user drags. On commit, the runtime calls Vue Router/browser back and reconciles with the route that actually wins.
A cold-start deep link has no in-memory predecessor. Route metadata can declare a logical parent, either as a route location or a function of the current route. That gives the runtime a synthetic predictive-back destination without pretending that a browser history entry exists.
Transactions
All navigation motion is represented by a NativeTransaction. A transaction identifies:
- Its kind: push, pop, sibling, present, or dismiss.
- The
fromandtoview keys. - Direction and presentation.
- Normalized progress and velocity.
- Whether commit uses push or replace semantics.
- Its lifecycle phase and optional component source geometry.
The important phases are interactive, committing, and settling. During the interactive phase, pointer movement directly controls progress. A release changes the phase and starts a damped spring toward either 0 or 1.
Velocity is measured as route progress per second rather than pixels per millisecond. A flick therefore behaves consistently on a small phone and a wide desktop window. High release velocity also advances the settling spring faster, so fast intent produces fast completion.
Transactions are interruptible. Starting a new navigation while a spring is settling does not wait for the old visual animation. The runtime waits only for an in-flight Vue Router history or guard decision, finalizes the old transaction at its authoritative endpoint, and begins the next transaction from that route. Transaction IDs prevent delayed pointer, preload, animation, and navigation callbacks from mutating a newer interaction.
Rendering and presentation
NativeRouterView keeps cached route entries as sibling layers. At rest, only the active layer is visible and interactive. During a transaction, exactly the from and to entries receive active roles.
Built-in presentations include push, reveal, adjacent-page slide, fade, modal, sheet, and no-motion. Sibling slide is deliberately different from a stack push: both pages move one screen-width for one screen-width of gesture progress, so the interaction feels like paging a continuous horizontal surface.
The runtime publishes progress as a CSS custom property. Built-in motion is mostly expressed through transforms and opacity, keeping per-frame JavaScript work constant. Applications can register presentations whose layer styles are functions of progress, role, direction, and optional source geometry.
First use has unavoidable setup work—downloading/evaluating a lazy chunk and mounting/layout of its Vue tree—but it does not need to compete with the transition. The runtime completes lazy resolution first, then gives newly mounted destinations a browser preparation frame before advancing progress. Full-surface dimming uses a composited opacity overlay rather than a changing CSS filter so WebKit does not repeatedly rasterize the route subtree.
Gesture ownership
Gesture recognition uses Pointer Events and waits for clear directional intent before claiming a pointer. Vertical scrolling remains available through touch-action, while form controls, editable content, and elements marked with data-native-gesture="ignore" are excluded.
Ownership is explicit:
- The application navigator owns the physical leading edge for back navigation.
- A component gesture link owns drags that begin on that component away from the back edge.
- A sheet dismissal surface owns downward vertical drags.
Pointer capture keeps delivery stable after recognition. Recognizer state is detached synchronously at pointer release, before route loading or animation promises are awaited. This is essential: a delayed callback from one gesture must never erase the state of a newer gesture.
History and cache are intentionally separate
Primary sibling routes normally replace one another in history. Their component trees can still remain mounted in the view cache. This means returning to a tab can preserve local UI state without making every tab selection a browser-back destination.
Siblings are mounted lazily on their first visit or gesture preview, not all at application startup. The cache has a configurable inactive-view limit and an explicit opt-out/pin policy. Popping a pushed route releases its component tree after the exit animation; a rejected cached guard target is evicted immediately. Older entries retain route descriptors but their component trees are unmounted and restored lazily. Durable application data should live in an application store rather than depending on a route component remaining cached forever.
A normal Vue <KeepAlive> is excellent when one outlet selects one child. It is not the cache primitive here because an interactive transition needs two independently addressed route instances to be active at once. Instead, the router owns those sibling view instances and exposes active, visible, cached, and eviction lifecycle signals. This preserves the useful KeepAlive distinction—mounted versus currently active—without coupling navigation history to Vue's single-child activation model.
Platform behavior
The core runtime is host-neutral. Platform adapters add capabilities that a browser-only router should not own:
- The PWA adapter reserves the leading edge in installed iOS standalone mode as early as web content allows.
- The Electron adapter disables Chromium overscroll history navigation and bridges host back/forward commands.
- The Capacitor adapter integrates hardware back, deep links, app lifecycle cancellation, root exit, and haptics.
This is progressive capability, not user-agent imitation. Normal browser tabs still work, but a browser may reserve gestures before JavaScript can claim them. Electron and Capacitor can disable or coordinate host behavior more deterministically.
Why this pattern is uncommon
There is prior work in mobile web navigation and animated router outlets, so the claim is not that interactive routing has never existed. What is unusual is combining Vue Router compatibility, live destination previews, reversible gestures, native-style history semantics, interruption, and multiple hosts in one reusable runtime.
Several factors make that combination rare:
- Web routers are commit-first. Their normal unit of work is “change the current location, then render it.” Native interaction needs “render the possible destination, let the user manipulate it, then decide whether location changes.”
- The platform does not expose a native navigation controller. Browser history, DOM rendering, pointer recognition, safe areas, service workers, and host gestures are separate systems with separate lifecycles.
- Two live routes complicate assumptions. Route injection, focus, accessibility, component side effects, caching, redirects, and scroll ownership all become more difficult when a route is visible but not current.
- Correct interruption is harder than animation. A polished demo can lock input while a transition runs. A native-feeling library must accept new intent during route loading, guard resolution, history mutation, and spring settling without stale asynchronous work winning.
- Host guarantees differ. A browser tab cannot promise the same edge ownership as a native
WKWebView, while Electron and Capacitor can change host settings. - The implementation cost is disproportionate. Most web products can accept non-interactive transitions. The additional state machine, testing matrix, memory use, and platform work are justified only when navigation feel is a core product requirement.
Native Vue Router addresses this by treating interactive navigation as its own stateful system while leaving Vue Router authoritative wherever Vue Router is strongest.
Scope
The library is a client-side navigation runtime, not a replacement for Vue Router and not a native rendering engine. It can closely reproduce native navigation composition and input behavior, but final fidelity still depends on application design, frame performance, platform embedding, typography, safe-area handling, and avoiding expensive work in route components during a gesture.