Page state & shallow routing
As you navigate around a SvelteKit app, you create history entries. Clicking the back and forward buttons traverses through this list of entries, re-running any load functions, replacing page components, and updating the scroll position and focused element as necessary.
Sometimes, it's useful to create history entries without performing a full navigation. We call this shallow routing.
For example, you might want to show a modal dialog that the user can dismiss by navigating back. This is particularly valuable on mobile devices, where swipe gestures are often more natural than interacting directly with the UI. In these cases, a modal that is not associated with a history entry can be a source of frustration, as a user may swipe backwards in an attempt to dismiss it and find themselves on the wrong page.
SvelteKit makes this possible with the goto function, which allows you to associate state with a history entry without navigating with the shallow: true option:
<script>
import { goto } from '$app/navigation';
import { page } from '$app/state';
import Modal from './Modal.svelte';
function showModal() {
goto('', {
state: { showModal: true },
shallow: true
});
}
</script>
<button onclick={showModal}>open</button>
{#if page.state.showModal}
<Modal close={() => history.back()} />
{/if}<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import Modal from './Modal.svelte';
function showModal() {
goto('', {
state: { showModal: true },
shallow: true
});
}
</script>
<button onclick={showModal}>open</button>
{#if page.state.showModal}
<Modal close={() => history.back()} />
{/if}State is globally accessible via the page object as page.state. You can make page state type-safe by declaring an App.PageState interface (usually in src/app.d.ts).
The modal can be dismissed by navigating back (unsetting page.state.showModal) or by interacting with it in a way that causes the close callback to run, which will navigate back programmatically.
You can also update the contents of the browser's URL bar during a shallow navigation:
function goto(url: string | URL, opts?: import("@sveltejs/kit").GotoOptions): Promise<void>Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
(as they would be with a regular navigation) or preserved.
Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) or the state change has been applied.
goto is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
For external URLs, use window.location = url to perform a full-page navigation instead of calling goto(url).
goto('/another/page', {
GotoOptions.state?: App.PageState | undefinedAn optional object that will be available as page.state.
state,
GotoOptions.shallow?: boolean | undefinedIf true, updates the URL and page.state without navigating.
shallow: true
});If the user reloads, they will land on
/another/page, not the currently rendered page — see Caveats.
Regardless of whether you choose to update the visible URL or not, beforeNavigate, onNavigate and afterNavigate will run with navigation.type === 'goto' and navigation.shallow === true.
Once shallow routing is active, page.shallow becomes a { url, params, route } object describing the page that would be rendered if the user were to navigate there (which would happen if, for example, they reloaded the page). page.url, page.params and page.route continue to describe the page that is currently rendered.
<script>
import { goto } from '$app/navigation';
import { page } from '$app/state';
</script>
<p>The user-visible URL is {page.shallow?.url.href ?? page.url.href}</p>
<p>The actual page you're on is {page.url.href}</p>
<button onclick={() => goto('/shallow', { shallow: true })}>enter shallow route</button><script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
</script>
<p>The user-visible URL is {page.shallow?.url.href ?? page.url.href}</p>
<p>The actual page you're on is {page.url.href}</p>
<button onclick={() => goto('/shallow', { shallow: true })}>enter shallow route</button>A regular goto call without shallow: true, or a standard link click, exits shallow routing.
Legacy mode
In SvelteKit 2 this functionality was achieved using
pushStateandreplaceState, which are now deprecated. Usegotowithshallow: trueinstead, and use thereplaceoption when replacing the current history entry.
Routing options
By default, the above examples create a new navigation entry in the history stack. If you don't want that, you can replace the existing navigation entry instead:
function goto(url: string | URL, opts?: import("@sveltejs/kit").GotoOptions): Promise<void>Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
(as they would be with a regular navigation) or preserved.
Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) or the state change has been applied.
goto is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
For external URLs, use window.location = url to perform a full-page navigation instead of calling goto(url).
goto(const url: URLurl, {
GotoOptions.state?: App.PageState | undefinedAn optional object that will be available as page.state.
state,
GotoOptions.replace?: boolean | undefinedIf true, replaces the current history entry rather than creating a new one.
replace: true
});By default, state set with goto is not restored after a reload. To change that, use persistState:
function goto(url: string | URL, opts?: import("@sveltejs/kit").GotoOptions): Promise<void>Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
(as they would be with a regular navigation) or preserved.
Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) or the state change has been applied.
goto is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
For external URLs, use window.location = url to perform a full-page navigation instead of calling goto(url).
goto(const url: URLurl, {
GotoOptions.state?: App.PageState | undefinedAn optional object that will be available as page.state.
state,
GotoOptions.persistState?: boolean | undefinedIf true, page.state will be restored after a full page reload.
persistState: true
});Shallow navigations preserve the current scroll position and focused element by default. You can opt out of either behavior with noScroll: false or keepFocus: false:
function goto(url: string | URL, opts?: import("@sveltejs/kit").GotoOptions): Promise<void>Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
(as they would be with a regular navigation) or preserved.
Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) or the state change has been applied.
goto is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
For external URLs, use window.location = url to perform a full-page navigation instead of calling goto(url).
goto(const url: URLurl, {
GotoOptions.shallow?: boolean | undefinedIf true, updates the URL and page.state without navigating.
shallow: true,
GotoOptions.noScroll?: boolean | undefinedIf true, preserves the browser's scroll position.
noScroll: false,
GotoOptions.keepFocus?: boolean | undefinedIf true, keeps the currently focused element focused.
keepFocus: false
});
page.stateis only populated after JavaScript loads, which can cause flickering UI. Use it carefully.
Loading data for a route
When shallow routing, you may want to render another +page.svelte inside the current page. For example, clicking on a photo thumbnail could pop up the detail view without navigating to the photo page.
For this to work, you need to load the data that the +page.svelte expects. A convenient way to do this is to use preloadData inside the click handler of an <a> element. If the element (or a parent) uses data-sveltekit-preload-data, the data will have already been requested, and preloadData will reuse that request.
<script>
import { preloadData, goto } from '$app/navigation';
import { page } from '$app/state';
import Modal from './Modal.svelte';
import PhotoPage from './[id]/+page.svelte';
let { data } = $props();
</script>
{#each data.thumbnails as thumbnail}
<a
href="/photos/{thumbnail.id}"
onclick={async (e) => {
if (innerWidth < 640 // bail if the screen is too small
|| e.shiftKey // or the link is opened in a new window
|| e.metaKey || e.ctrlKey // or a new tab (mac: metaKey, win/linux: ctrlKey)
// should also consider clicking with a mouse scroll wheel
) return;
// prevent navigation
e.preventDefault();
const { href } = e.currentTarget;
// run `load` functions (or rather, get the result of the `load` functions
// that are already running because of `data-sveltekit-preload-data`)
const result = await preloadData(href);
if (result.type === 'loaded' && result.status === 200) {
goto(href, { shallow: true, state: { selected: result.data } });
} else {
// something bad happened! try navigating
goto(href);
}
}}
>
<img alt={thumbnail.alt} src={thumbnail.src} />
</a>
{/each}
{#if page.state.selected}
<Modal onclose={() => history.back()}>
<!-- pass page data to the +page.svelte component,
just like SvelteKit would on navigation -->
<PhotoPage data={page.state.selected} />
</Modal>
{/if}<script lang="ts">
import { preloadData, goto } from '$app/navigation';
import { page } from '$app/state';
import Modal from './Modal.svelte';
import PhotoPage from './[id]/+page.svelte';
let { data } = $props();
</script>
{#each data.thumbnails as thumbnail}
<a
href="/photos/{thumbnail.id}"
onclick={async (e) => {
if (innerWidth < 640 // bail if the screen is too small
|| e.shiftKey // or the link is opened in a new window
|| e.metaKey || e.ctrlKey // or a new tab (mac: metaKey, win/linux: ctrlKey)
// should also consider clicking with a mouse scroll wheel
) return;
// prevent navigation
e.preventDefault();
const { href } = e.currentTarget;
// run `load` functions (or rather, get the result of the `load` functions
// that are already running because of `data-sveltekit-preload-data`)
const result = await preloadData(href);
if (result.type === 'loaded' && result.status === 200) {
goto(href, { shallow: true, state: { selected: result.data } });
} else {
// something bad happened! try navigating
goto(href);
}
}}
>
<img alt={thumbnail.alt} src={thumbnail.src} />
</a>
{/each}
{#if page.state.selected}
<Modal onclose={() => history.back()}>
<!-- pass page data to the +page.svelte component,
just like SvelteKit would on navigation -->
<PhotoPage data={page.state.selected} />
</Modal>
{/if}Caveats
Shallow routing requires JavaScript. Be mindful when using it and try to think of sensible fallback behaviour in case JavaScript isn't available.
The server has no awareness of history state. As such, page.state is an empty object during SSR, and a reload will navigate directly to the prior page.shallow.url if it exists. In other words, if page.shallow.url.pathname is /photos/123 before the reload, after the reload page.url.pathname will be /photos/123 and page.shallow will be null, regardless of the persistState option. (This only applies to the initial page load, to avoid a flickering UI when the client app starts.)
Edit this page on GitHub llms.txt