docs / Native

Native Routing

File-based and manual routing in vesk-native: layout/page conventions, dynamic params, manual routes in veskconfig.ts, Link/NavLink/Outlet, and programmatic navigation.

Vesk Native uses file-based routing by default — the folder structure under `app/` maps directly to navigation routes. Manual routes can be added in `veskconfig.ts` for cases that don't fit the filesystem.

File-based routing

Place `.vsk` files in `app/` to create routes. The convention is identical to the web framework:

File path Route
app/page.vsk / (root)
app/about/page.vsk /about
app/blog/page.vsk /blog
app/blog/[slug]/page.vsk /blog/:slug
app/shop/[id]/page.vsk /shop/:id
app/layout.vsk Layout wrapper (not a route)
  • `layout.vsk` is skipped as a route — it wraps child routes via `{props.children}`.
  • `page.vsk` inside a folder maps to that folder's path.
  • `[paramName]` folders create dynamic route segments — the param is extracted from the URL.
  • Nested folders create nested paths: `app/blog/[slug]/page.vsk` → `/blog/:slug`.

Layout file

app/layout.vsk
									
										component Layout(props) {
  <div class="flex-col w-full h-full">
    <nav class="bg-white px-4 py-2 border-b">
      <NavLink href="/">
        <span class="text-base font-bold">MyApp</span>
      </NavLink>
      <div class="flex gap-3">
        <NavLink href="/about" class="text-sm">About</NavLink>
        <NavLink href="/shop" class="text-sm">Shop</NavLink>
      </div>
    </nav>
    <main class="flex-1">
      {props.children}
    </main>
  </div>
}
									
								

Dynamic params

Use `[paramName]` in the folder name to create a dynamic segment. Access the param value with `useParams()` in the page script:

app/blog/[slug]/page.vsk
									
										export component BlogPost() {
  const params = useParams()
  const slug = params.slug

  <div class="p-4">
    <h1 class="text-xl font-bold">Post: {slug}</h1>
  </div>
}
									
								

`useParams()` returns a `Record<string, string>` with the dynamic segment values. All values are strings.

Manual routes

For routes that don't fit the filesystem, add them to `veskconfig.ts` under `routes`. Manual routes use `component` to reference a component name defined in a `.vsk` file:

veskconfig.ts
									
										import { defineConfig } from '@vesk/native'

export default defineConfig({
  // ... other config
  routes: [
    { path: '/settings', component: 'SettingsPage' },
    { path: '/profile/:userId', component: 'ProfilePage' },
  ],
})
									
								
  • Manual routes are merged with file-based routes — file-based routes take precedence for the same path.
  • The `component` value is the name of a component declared in any `.vsk` file in your app.
  • Dynamic segments use `:paramName` syntax — same as file-based `[paramName]` folders.
  • Manual routes support `exitOnBack` to mark the route as a double-back-to-exit page.
app/components/Nav.vsk
										
											component Nav() {
  <div class="flex gap-3 p-4">
    <Link href="/about">
      <span class="text-blue-600">About</span>
    </Link>
    <NavLink href="/shop" class="text-gray-600">
      Shop
    </NavLink>
  </div>
}
										
									
  • `<Link href="/path">` — a navigation link. Tap navigates to the route.
  • `<NavLink href="/path" class="...">` — a link that also receives an active class when the route matches.
  • Both accept `class` for styling.
  • `<Outlet />` renders the child route inside a layout — use it in `layout.vsk` where you want `{props.children}` to appear.

Programmatic navigation

In page scripts, navigate imperatively with the global helpers or the router hook:

Function Behavior
navigate('/path') Push a new route onto the navigation stack.
goBack() Pop the navigation stack. Returns false at the root.
useParams() Return the current route's dynamic params as Record<string, string>.
useQuery() Return the current route's query string as Record<string, string>.
useRouter() Return the full router object: { currentPath, navigate, back, refresh }.
app/profile/page.vsk
									
										export component ProfilePage() {
  const params = useParams()
  const query = useQuery()
  const router = useRouter()

  <div class="p-4">
    <h1>Profile: {params.userId}</h1>
    <p>Tab: {query.tab}</p>
    <button onClick={() => goBack()} class="mt-4 text-blue-600">
      Go back
    </button>
    <button onClick={() => navigate('/settings')} class="mt-2 text-blue-600">
      Settings
    </button>
  </div>
}
									
								

Back navigation behavior

The back behavior is configured in `veskconfig.ts` under `back`:

  • `mode: 'stack'` — custom stack-based navigation with double-back-to-exit.
  • `mode: 'system'` — delegates to the OS back button (Android default).
  • `doubleBackToExit: true` — tapping back twice within `exitDelayMs` exits the app.
  • `exitRoutes` — routes where a double-back exits; interior routes always pop the stack first.
  • `exitRoutes` defaults to the root page (`/`) — add `{ path: '/shop', exitOnBack: true }` in manual routes to change this.

Scroll restoration

The router automatically saves and restores scroll position per route. Each route's scroll state is stored in the `NavController` and restored when navigating back.

app/components/ScrollList.vsk
									
										export component ScrollList() {
  const scrollState = rememberRouteScrollState(0)

  <div class="overflow-y-auto flex-1" scrollState={scrollState}>
    {props.children}
  </div>
}
									
								

Generated Kotlin router

The build generates a `Router.kt` with a `NavController` class that holds the navigation state as Compose `mutableStateOf` properties. Route matching strips `?query` and `#fragment` segments, matches `{param}` segments, and returns `null` for unknown routes (renders nothing).

The `navigation-native` npm package provides the JS-side types (`RouteConfig`, `RouterState`, `createRouter`) — the real navigation is the generated Kotlin `NavController` running on the device.