Navigation
Programmatic and link-based navigation in Bosia — goto(), beforeNavigate, afterNavigate, form actions, and the full-reload escape hatch.
Bosia ships four navigation patterns that mirror SvelteKit:
- Plain
<a href>links — the router intercepts. goto()— programmatic SPA navigation.- Form action
redirect(303, '/x')— server-driven post-submit nav. window.location.href— full browser reload (escape hatch).
The first three are SPA — they don't re-execute the entry script. The fourth tears down and re-hydrates.
Anchor links
<a href="/dashboard">Dashboard</a>Bosia's click handler intercepts plain anchor clicks and runs router.navigate(). Modifier-clicks (Cmd, Ctrl, middle-click) and target="_blank" fall through to the browser, so "open in new tab" works as expected. rel="external" and download also opt out.
`goto()`
import { goto } from "bosia/client";
await goto("/dashboard");
await goto("/login", { replaceState: true, invalidateAll: true });goto() returns a Promise that resolves after the navigation settles (loaders ran, components mounted).
Options
| Option | Default | Description |
|---|---|---|
replaceState |
false |
Use history.replaceState instead of pushState. |
invalidateAll |
false |
Mark every loader dirty so the destination re-runs all server loaders. |
noScroll |
false |
Skip the default scroll-to-top after navigation. |
keepFocus |
false |
Reserved — not yet honored. |
state |
— | Reserved — Bosia has no shallow routing yet. |
If the URL matches the current route, goto() resolves immediately without re-running loaders. The invalidateAll: true option is not honored on same-path calls — call invalidateAll() directly (imported from bosia/client) to refresh in place.
Scroll behavior
- Forward navigation (link,
goto(), form redirect) scrolls to the top — or to the#hashelement if the destination URL has one.goto(url, { noScroll: true })skips this for one navigation. - Back/forward (popstate) restores the scroll position the page had when you left it. The router saves the position per history entry and restores it after the destination page renders, so long listings don't snap to the top when the user presses Back. Positions survive reloads via
sessionStorage.
This is automatic — no app code needed.
Snapshots
Scroll restoration brings back the position; snapshots bring back the state — an in-progress form field, an open accordion, a search box. Export a snapshot object from +page.svelte:
<script lang="ts">
import type { Snapshot } from "./$types";
let comment = $state("");
export const snapshot: Snapshot<string> = {
capture: () => comment,
restore: (value) => (comment = value),
};
</script>
<textarea bind:value={comment}></textarea>capture() runs when the user navigates away; restore(value) runs when they come back via the browser's Back/Forward buttons. Snapshots also survive a full page reload — they persist to sessionStorage on unload.
Rules:
- The captured value must be JSON-serializable (no functions, Dates become strings) — that's how it survives reloads.
- Forward navigation to a page never restores — only Back/Forward (and reload) do, matching SvelteKit.
- Only
+page.sveltesupportssnapshot(not layouts).
Snapshot<T> is also importable from bosia/client.
Lifecycle hooks
beforeNavigate runs before each client-side navigation. The callback may call nav.cancel() to block the navigation (except on browser back/forward — see below).
<script lang="ts">
import { beforeNavigate, afterNavigate } from "bosia/client";
beforeNavigate((nav) => {
if (hasUnsavedChanges && !confirm("Discard changes?")) {
nav.cancel();
}
});
afterNavigate((nav) => {
console.log("navigated to", nav.to?.url.pathname);
});
</script>Both auto-unregister when the calling component is destroyed.
The Navigation object
interface Navigation {
from: { url: URL; params: Record<string, string> } | null;
to: { url: URL; params: Record<string, string> } | null;
type: "link" | "goto" | "popstate" | "form" | "enter";
willUnload: boolean;
cancel: () => void;
}type— how the navigation was triggered.willUnload—truewhen the browser is about to tear down the page (closed tab, external link). For these eventscancel()is a no-op; useaddEventListener("beforeunload", ...)directly to prompt the user.cancel()on apopstate(back/forward) navigation is also a no-op — the browser has already moved its history pointer by the time the listener runs.
Loading indicators
Bosia shows a thin progress bar at the top of the page during every client navigation. For a richer transition, drop a +loading.svelte into a route folder and Bosia renders that skeleton automatically while the destination loads — nested inside the shared layouts, no beforeNavigate wiring needed. See Loading Skeletons.
Form actions
Form actions that throw redirect(303, "/x") navigate via the router automatically when wrapped with use:enhance:
<form method="POST" action="?/logout" use:enhance>
<button>Log out</button>
</form>See Form Actions for the full story.
`window.location.href` — full reload
Use only when you genuinely need to tear down all client state (e.g. logging out, switching tenants in a multi-tenant app where a fresh runtime context is desired):
function hardLogout() {
document.cookie = "session=; Max-Age=0";
window.location.href = "/";
}For internal navigation prefer goto() — it's faster (no script re-parse, no re-hydration cost) and preserves the loader cache where possible.