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.
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.
- Open or create
src/routes/+layout.svelte. - 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:fadeapplies 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.
- Create
src/routes/+layout.ts:
export const load: LayoutLoad = async ({ url }) => {
const { pathname } = url;
return {
pathname
};
}; - Use
pathnamein+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.