MANGGA SANTAI WAE HATUR NUHUN GASKEUN MANGGA SANTAI WAE HATUR NUHUN GASKEUN

Article

Easy Page Transitions in SvelteKit

How to add smooth page transitions to a SvelteKit app using the built-in transition and easing APIs, no extra packages needed.

Easy Page Transitions in SvelteKit
M
Muhammad Surya J

Overview

Smooth page transitions make a web app feel a lot more polished, and SvelteKit makes them easy thanks to Svelte’s built-in transition and animation APIs. Here’s how to add page transitions to a SvelteKit app, step by step.

Prerequisites

Before diving in, make sure you have:

  • Basic knowledge of Svelte and SvelteKit.
  • A working SvelteKit project. If you don’t have one yet, spin one up:
    npx sv create my-sveltekit-app
    cd my-sveltekit-app
    npm install
    npm run dev

Step 1: No extra packages needed

We’re using svelte/transition and svelte/easing, both of which ship with Svelte already. Nothing to install.

Step 2: Set up the layout component

To get transitions running across all pages, edit +layout.svelte, which wraps every route in the app.

  1. Open or create src/routes/+layout.svelte.
  2. Add this to wire up a basic transition:
<script>
	import { fade } from 'svelte/transition';
	let { children, data } = $props();
	let { pathname } = $derived(data);
</script>

{#key pathname}
	<div transition:fade>
		{@render children()}
	</div>
{/key}

What’s happening here:

  • transition:fade applies a fade effect when switching pages.
  • {@render children()} is where the current page’s content renders.

This won’t work yet, though, since pathname isn’t defined. That comes from the load function.

Step 3: Grab the pathname with load

The load function gives you access to the current URL. Use it to pass pathname into the layout.

  1. Create src/routes/+layout.ts:
export const load: LayoutLoad = async ({ url }) => {
	const { pathname } = url;

	return {
		pathname
	};
};
  1. Use pathname in +layout.svelte:
<script>
	import { fade } from 'svelte/transition';
	let { children, data } = $props();
	let { pathname } = $derived(data);
</script>

{#key pathname}
	<div in:fade={{ duration: 300, delay: 400 }} out:fade={{ duration: 300 }}>
		{@render children()}
	</div>
{/key}

in:fade and out:fade set up separate enter and exit animations with their own timing. The delay gives the outgoing page a moment to clear before the new one fades in.

Step 4: Add easing

Easing curves make the motion feel less mechanical. Svelte ships a handful of them.

Import easing from svelte/easing:

<script>
	import { fade } from 'svelte/transition';
	import { cubicIn, cubicOut } from 'svelte/easing';
	let { children, data } = $props();
	let { pathname } = $derived(data);
</script>

{#key pathname}
	<div
		in:fade={{ easing: cubicOut, duration: 300, delay: 400 }}
		out:fade={{ easing: cubicIn, duration: 300 }}
	>
		{@render children()}
	</div>
{/key}

cubicIn and cubicOut give the transition a more natural acceleration curve instead of a flat linear fade.

Step 5: Try the fly effect

Swap fade for fly if you want the page to move as it transitions, not just fade.

<script>
	import { fly } from 'svelte/transition';
	import { cubicIn, cubicOut } from 'svelte/easing';
	let { children, data } = $props();
	let { pathname } = $derived(data);
</script>

{#key pathname}
	<div
		in:fly={{ easing: cubicOut, y: 10, duration: 300, delay: 400 }}
		out:fly={{ easing: cubicIn, y: -10, duration: 300 }}
	>
		{@render children()}
	</div>
{/key}

y: 10 and y: -10 control which direction the page slides in from and out toward.

Optional: add a loading bar

If your routes do any data fetching, a loading bar covers the gap so the page doesn’t feel frozen mid-navigation.

<script>
	import { beforeNavigate, afterNavigate } from '$app/navigation';

	let isLoading = $state(false);
	let loadingProgress = $state(0);
	let loadingInterval: ReturnType<typeof setInterval> | null = null;

	beforeNavigate(({ to }) => {
		if (to?.route.id) {
			isLoading = true;
			loadingProgress = 0;

			loadingInterval = setInterval(() => {
				loadingProgress = Math.min(90, loadingProgress + 5);
			}, 100);
		}
	});

	afterNavigate(() => {
		if (loadingInterval) {
			clearInterval(loadingInterval);
			loadingInterval = null;
		}

		loadingProgress = 100;

		setTimeout(() => {
			isLoading = false;
		}, 300);
	});
</script>

{#if isLoading}
	<div class="absolute inset-x-0 top-0 z-10">
		<div class="h-1 w-full bg-slate-200 dark:bg-slate-700">
			<div
				class="h-full animate-pulse bg-slate-700 transition-all duration-300 dark:bg-slate-400"
				style="width: {loadingProgress}%"
			></div>
		</div>
	</div>
{/if}

beforeNavigate and afterNavigate toggle the loading state around each navigation.

This site uses the same setup

The page you’re reading this on runs the exact pattern described above, fade transitions plus a loading bar while the next route resolves.

Wrapping up

Page transitions are one of the cheaper ways to make a SvelteKit app feel more finished, and Svelte’s built-in transition and easing APIs get you there without adding a dependency. Try a few combinations of effects and timings until one feels right for your app.