Documentation

$navigate

Navigate between screens programmatically.

Basic Usage

$navigate("/login")
$navigate("/profile/${userId}")
$navigate("/home")

A literal path is checked against src/routes/ at build time; a typo is a build error. A value is resolved when it is used, and an unknown path opens the not-found screen:

$navigate(link.path)

With Path Parameters

Pass dynamic values in the URL:

<Button onClick={() => $navigate("/user/${user.id}")}>
  View Profile
</Button>

With State

Pass data to the next screen without adding it to the URL:

$navigate("/show/${id}", state = {
  name: show.name,
  poster: show.poster
})

// In the destination screen
val previewName = $screen.state?.name
val previewPoster = $screen.state?.poster

This is useful for:

  • Preview data while loading full details
  • Passing non-serializable data between screens
  • Avoiding long URLs

state is preview data for the next composition. A deep link or a plain $navigate passes none, so read it with ?. and a fallback, and put anything the screen needs in a loader.

The back stack and loaders

Navigating to a route that is already on the back stack returns to it; the entries above it are popped. When the target has a loader, the loader runs first and the current screen stays up until it has finished, as SvelteKit does.

$navigate("/feed", reload = true)   // navigate at once, even to the route on screen

reload = true skips the wait and navigates immediately, even to the route already showing. Use it for a pull-to-refresh that should visibly reset.

See it happen

Switch between these to see what each option changes in the generated call.

ShowButton.wh
import $lib.components.ui.Button

@prop val id: String

fun openShow() {
  $navigate("/show/${id}")
}

<Button onClick={() => openShow()}>
  <Text>View show</Text>
</Button>
Kotlin waiting
Scroll into view to compile.

A literal path is checked against src/routes/ at build time, so a typo is a build error.

File-Based Routing

Routes are automatically generated from your src/routes/ folder structure:

routes/
├── +screen.wh              # → /
├── login/+screen.wh         # → /login
├── user/[id]/+screen.wh     # → /user/:id
└── posts/[slug]/+screen.wh  # → /posts/:slug

Dynamic Routes

Use [param] folders for dynamic segments:

// Navigate to user profile
$navigate("/user/123")

// In user/[id]/+screen.wh
val userId = $screen.params.id  // "123"

Navigating from a list

A row that only navigates is a link: write href, not an onClick that calls $navigate. href works on any tappable element — a Row, a Card, a Button. Writing both on one element is a build error.

<Column class="gap-8">
  @for (show in shows) {
    <Card href="/show/${show.id}">
      <Image src={show.poster} />
      <Text>{show.name}</Text>
    </Card>
  }
</Column>

Reach for $navigate itself only for the imperative case: after a handler does something first, or from outside composition entirely (a loader, $onMount, a callback that runs after an async call).

Layouts

Wrap screens with shared UI using +layout.wh:

// routes/+layout.wh (root layout)
<Scaffold>
  <TopAppBar title="My App" />
  <slot />  // screen content goes here
  <BottomNavigation>
    <BottomBarItem href="/" selected={$screen.path == "/"}>Home</BottomBarItem>
    <BottomBarItem href="/search" selected={$screen.path == "/search"}>Search</BottomBarItem>
    <BottomBarItem href="/profile" selected={$screen.path == "/profile"}>Profile</BottomBarItem>
  </BottomNavigation>
</Scaffold>

Breaking Layout Inheritance

Skip layouts for specific screens:

  • +screen@.wh - No layouts at all
  • +screen@root.wh - Only root layout, skip intermediate layouts

Back Navigation

The Android back button already works. To pop the stack from code:

$navigate.back()

It works outside composition too — a loader, $onMount, a callback after an async call — through the same generated queue every other out-of-composition $navigate call uses. The back-stack-manipulating variants, $navigate.replace and $navigate.popUpTo, stay composition-only.

See Also