$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.
import $lib.components.ui.Button
@prop val id: String
fun openShow() {
$navigate("/show/${id}")
}
<Button onClick={() => openShow()}>
<Text>View show</Text>
</Button>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
- $screen - Access route parameters
- $data - Load data for screens
- Project Structure - File-based routing