History and the host
A mini app lives in a webview iframe: it has no address bar of its own, and “back” is a system gesture or a host button. The package owns three things: where the URL lives, how the host learns about it, and when the native swipe-back is on.
History modes
Section titled “History modes”createVkHistory from the core layer:
createVkHistory(); // "hash" (default): #/meeting/7createVkHistory({ mode: "hash-param" }); // #?path=/meeting/7// "browser": omit history in createRouter — TanStack takes its defaulthash— the hash IS the location. Relevant when the host does not let you own the path.hash-param— the path rides in thepathhash param (rename it withpathParam). Foreign hash params are preserved,location.searchis untouched.browser— the plain browser history; just do not passhistorytocreateRouter.
A deep link written in the other hash mode resolves too (legacy tolerance) — old shared links keep working. The history is built on TanStack’s createBrowserHistory — blockers and popstate work as in plain TanStack.
Host sync
Section titled “Host sync”createBridgeSync wires the history and the host in both directions:
import { createBridgeSync } from "@vkma.dev/router";
createBridgeSync({ history: router.history, bridge });- Every committed history change goes to the host as
VKWebAppSetLocationwithreplace_state: true— the share URL and the host’s back reflect the app state. The startup location is sent once at creation. - Incoming
VKWebAppChangeFragmentandVKWebAppLocationChanged(host-initiated jumps) becomehistory.replace(href). An echo of your own location is ignored — no loop. - The host’s “back” is
VKWebAppCloseterritory: ChangeFragment is not a fragment pop.
The payload codecs depend on the mode: hash sends the hash content, hash-param sends ?path=… preserving foreign params. Call the sync after the router exists (not in module scope next to its declaration) — otherwise a module cycle dies with a TDZ crash.
Swipe-back
Section titled “Swipe-back”createSwipeBackManager drives the native gesture:
import { createSwipeBackManager } from "@vkma.dev/router";
createSwipeBackManager({ history: router.history, bridge });The automatic rule: the gesture is enabled while the app can go back in its own history and disabled on the first entry — there the system back must close the app. The implementation sends VKWebAppSetSwipeSettings { history } plus VKWebAppEnableSwipeBack/VKWebAppDisableSwipeBack; web error code 6 is silenced.
The hard opt-out (an app that drives the gesture itself): createSwipeBackManager({ enabled: false }) — it wins over everything below.
Per-panel override
Section titled “Per-panel override”useEnableSwipeBack(false, panel);Pass false on surfaces where the gesture interferes (drawing pads, drag surfaces), and always pass the second argument — the panel handle. Panels are forever-mounted static children: without the binding the override sticks to whichever panel mounted last, while with the ref it applies only while that panel is the deepest matched one and is released on navigation away. true forces the gesture even on the first entry — the system back then closes the app.
Overlays on top of the history — in modals and popouts.