Svelte logo

SvelteKit by Example: URL & Navigation State

The URL can hold state that should survive a reload or be shared with someone else. $app/state exposes the current page and any navigation in progress.

In this example, we'll put our completed-todo filter in a search parameter.

page is reactive. Use $derived when calculating values from it so they update after navigation.

This page receives { todos } from the universal load in the refreshing data example, with the fields from the API endpoints example.

A normal link sets the filter. /todos?completed=true shows all todos; /todos shows only unfinished ones.

src/routes/todos/+page.svelte
<script>
  import { page } from '$app/state';
  let { data } = $props();

  let showCompleted = $derived(
    page.url.searchParams.get('completed') === 'true'
  );


  let visibleTodos = $derived(
    data.todos.filter((todo) => showCompleted || !todo.completed)
  );
</script>




<a href={showCompleted ? '/todos' : '/todos?completed=true'}>
  {showCompleted ? 'Hide completed' : 'Show completed'}
</a>

{#each visibleTodos as todo (todo.id)}
  <p>{todo.task}: {todo.completed ? 'Done' : 'To do'}</p>
{/each}

Compare the exact route ID for an active link. /todos/[id] won't mark the /todos link as active.

navigating.to is set while a navigation is in progress, and is null otherwise.

src/routes/+layout.svelte
<script>
  import { page, navigating } from '$app/state';
  let { children } = $props();
</script>

<nav>
  <a
    href="/todos"
    aria-current={page.route.id === '/todos' ? 'page' : undefined}
  >Todos</a>
  <a href="/about">About</a>
</nav>





{#if navigating.to}
  <p>Loading…</p>
{/if}

{@render children()}

Prefer links for ordinary navigation. Use goto when navigation follows an interaction, like changing this checkbox.

Copy the current URL before changing it. This preserves other search parameters and the hash.

In SvelteKit 3, replace replaces the history entry and reset: false preserves scroll and focus. goto returns a promise; show navigation failures instead of leaving them unhandled.

src/lib/TodoFilter.svelte
<script>
  import { page } from '$app/state';
  import { goto } from '$app/navigation';

  let message = $state('');

  async function changeFilter(event) {
    message = '';






    const url = new URL(page.url);
    if (event.currentTarget.checked) {
      url.searchParams.set('completed', 'true');
    } else {
      url.searchParams.delete('completed');
    }

    try {

      await goto(url, { replace: true, reset: false });
    } catch {
      message = 'Could not change the filter.';
    }
  }
</script>

<label>
  <input
    type="checkbox"
    checked={page.url.searchParams.get('completed') === 'true'}
    onchange={changeFilter}
  >
  Show completed
</label>
<p role="status">{message}</p>

Navigation Hooks

Register hooks during component initialization. This separate draft field guards against losing unsaved text.

beforeNavigate can cancel navigation. For a tab close, cancelling requests the browser's native confirmation instead.

afterNavigate runs on mount and after navigation. Here it records the last visited path in a status message.

src/lib/TodoDraft.svelte
<script>
  import { beforeNavigate, afterNavigate } from '$app/navigation';

  let task = $state('');
  let visited = $state('');



  beforeNavigate(({ cancel, willUnload }) => {
    if (!task.trim()) return;






    if (willUnload || !confirm('Discard your draft?')) {
      cancel();
    }
  });




  afterNavigate(({ to }) => {
    visited = to.url.pathname;
  });
</script>

<label>Draft todo <input bind:value={task}></label>
<p role="status">Last visited: {visited}</p>