Simple page transitions in Svelte

15 May 2020

Not yet updated to reflect migration to SvelteKit.

What we’re making

We’re going to replicate this very website’s page transition. It’s a technique adapted from this Stack Overflow thread.

We’ll again be using Sapper for this tutorial so go ahead and clone its starter template. Note that this technique isn’t exclusive to Sapper, it’s just what I’m comfortable with.

TerminalShell
npx degit "sveltejs/sapper-template#rollup" my-app
cd my-app
npm install

# wait

npm run dev

The transition component

Create a transition component that you’ll wrap your routes with.

src/components/PageTransition.svelteSvelte
<script>
	import { cubicInOut } from 'svelte/easing'
	let duration = 200
	let delay = duration

	const transitionIn = () => ({
		duration,
		delay,
		easing: cubicInOut,
		css: (t) => `opacity: ${t}`,
	})

	const transitionOut = () => ({
		duration,
		delay: 0,
		easing: cubicInOut,
		css: (t) => `opacity: ${t}`,
	})
</script>

<div in:transitionIn out:transitionOut>
	<slot />
</div>

What each line of code does is out of the scope of this post. You can learn more from the official tutorial and/or doc.

Using the transition component

Wrap all of your routes (including _error.svelte, but not _layout.svelte) this way:

src/routes/index.svelteSvelte
<script>
	import PageTransition from '../components/PageTransition.svelte'
</script>

<style>
	/* … */
</style>

<svelte:head>
	<title>Sapper project template</title>
</svelte:head>

<PageTransition>
	<h1>Great success!</h1>

	<!-- … -->
</PageTransition>

For blog/[slug].svelte, hardcode some links to test transition from one blog post to another.

src/routes/blog/[slug].svelteSvelte
<script context="module">
	// …
</script>

<script>
	import PageTransition from '../../components/PageTransition.svelte'
	export let post
</script>

<style>
	/* … */
</style>

<svelte:head>
	<title>{post.title}</title>
</svelte:head>

<PageTransition>
	<a href="/blog/what-is-sapper" rel="prefetch">Go to post A</a>
	<a href="/blog/how-to-use-sapper" rel="prefetch">Go to post B</a>

	<h1>{post.title}</h1>

	<div class="content">{@html post.html}</div>
</PageTransition>

You should see that there is transition when you go from blog index to blog post, but not from post A to post B. We’ll fix this next.

Special treatment for dynamic routes

Think about what happens when you move between posts. The route component isn’t ‘destroyed and remounted’. Instead, the post prop merely changes, causing:

  • title and h1 to react to a new post.title, and
  • the markup inside div.content to react to a new post.html.

PageTransition stays the same and its in and out transitions are not triggered.

You know that the posts differ, and their slug is unique among them. Svelte’s way of assigning identity is through keyed each blocks. We’re going to use the post’s slug as key.

Wrap the markup with this keyed each block.

src/routes/blog/[slug].svelteSvelte
{#each [post] as p (p.slug)} <!-- add this -->
	<PageTransition>
		<a href="/blog/what-is-sapper" rel="prefetch">Go to post A</a>
		<a href="/blog/how-to-use-sapper" rel="prefetch">Go to post B</a>

		<h1>{p.title}</h1> <!-- change this -->

		<div class="content">
			{@html p.html} <!-- and this -->
		</div>
	</PageTransition>
{/each} <!-- and add this -->

each takes an array. In this case we’re giving it an array of one (the post prop), declaring slug as its identity (key). When the identity changes (you navigate to another blog post), the whole tree is invalidated, causing transition to take place.