Preserving State During In-App Navigation in SvelteKit
SvelteKit provides a built-in Snapshots feature that captures ephemeral DOM state like scroll positions and input values. When a user fills out a form, navigates away, and then returns, their input can be automatically restored.
How Built-in Snapshots Work
You export a snapshot object from your +page.svelte with capture and restore methods:
<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} />
The capture function runs when leaving the page, and restore runs when returning. The data is persisted to sessionStorage and tied to the browser's history stack.
The Limitation
There's a catch: built-in Snapshots only work when navigating via the browser's history stack, such as when the user leaves your site entirely and returns using the back button, or when refreshing the page. They do not trigger during normal in-app navigation between routes.
If a user is on /posts/new, clicks a link to /posts, and then navigates back to /posts/new, the built-in snapshot will not restore their draft. This is because SvelteKit's client-side router handles the navigation without touching the browser's history-based snapshot mechanism.
An In-App Alternative
To preserve state during in-app navigation, you can use SvelteKit's navigation lifecycle hooks directly:
import { beforeNavigate, afterNavigate } from '$app/navigation';
export class Snapshot<T> {
constructor(capture: () => T, restore: (data: T) => void) {
beforeNavigate((n) => {
if (n.from?.url) {
this.setSession(n.from.url.pathname, capture());
}
});
afterNavigate((n) => {
if (n.to?.url) {
const data = this.getSession(n.to.url.pathname);
if (data) restore(data);
}
});
}
private setSession(key: string, value: T): void {
sessionStorage.setItem(key, JSON.stringify(value));
}
private getSession(key: string): T | null {
const item = sessionStorage.getItem(key);
if (item === null) return null;
return JSON.parse(item) as T;
}
}
Usage in a component:
<script lang="ts">
import { Snapshot } from '$lib/snapshot';
let formData = $state({ title: '', body: '' });
new Snapshot(
() => formData,
(data) => formData = data
);
</script>
<input bind:value={formData.title} placeholder="Title" />
<textarea bind:value={formData.body} placeholder="Body" />
This approach uses beforeNavigate to capture state keyed by the current URL pathname, and afterNavigate to restore it when returning to that route. The state persists in sessionStorage, surviving page refreshes as well.
When to Use This
This pattern is useful when you want to preserve transient UI state across navigation:
- Draft form content (blog posts, comments, support tickets)
- Partially completed multi-step wizards
- Search filters or query parameters the user has configured
- Scroll position within a long list or document
- Expanded/collapsed state of accordions or tree views
- Unsaved edits in an admin panel
Caveats
The data must be JSON-serializable. Avoid storing large objects, as they remain in sessionStorage for the duration of the session. For complex state that needs server persistence, consider saving drafts to your backend instead.