DEV Community

Evan Winter
Evan Winter

Posted on • Updated on

Simple Page Transitions with SvelteKit

Demo: https://sveltekit-page-transitions.netlify.app/
Source code: https://github.com/evanwinter/sveltekit-page-transitions

Overview

  1. Create a <PageTransition /> component that does a simple fade-out-and-in transition (using Svelte's built-in transition directive) when a given prop changes.

  2. Create a layout file, wrap its content with the <PageTransition /> component, and pass the current page's path to the component as a prop.

Step 1: Creating the <PageTransition /> component

Create a component file at src/lib/components/PageTransition.svelte.

Import the fly transition from Svelte's built-in transition library, and setup an element that flys in and out. Add a delay to the in transition that's equal to or greater than the duration of the out transition (otherwise, the user will see them happen at the same time).

Export a variable called url and wrap the element inside of a #{key ...} block.

The contents of the block will be destroyed and recreated only when the value of url changes.

When that happens, the out transition will be executed immediately, and the in transition happen once the out transition finishes –– creating a "fade-out-and-back-in" effect.

<!-- PageTransition.svelte -->
<script>
  import { fly } from "svelte/transition";
  export let url = "";
  const pageTransitionDuration = 500;
</script>

{#key url}
  <div in:fly={{  x:-5, duration: pageTransitionDuration, delay: pageTransitionDuration }}
       out:fly={{ x: 5, duration: pageTransitionDuration }}>
    <slot />
  </div>
{/key}
Enter fullscreen mode Exit fullscreen mode

Step 2: Using the <PageTransition /> component

Create a layout file at src/routes/__layout.svelte.

<!-- __layout.svelte -->

<!-- 1. Using a `load` function, pass the current URL to the layout component as a prop -->
<script context="module">
  /** @type {import('@sveltejs/kit').Load} */
  export const load = async ({ url }) => ({ props: { url } });
</script>

<script>
  import PageTransition from "$lib/components/PageTransition.svelte";
  export let url;
</script>

<div>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>

  <!-- 2. Pass the `url` prop to the `PageTransition` component, causing it to re-render when the page changes -->
  <PageTransition {url}>
    <slot />
  </PageTransition>
</div>
Enter fullscreen mode Exit fullscreen mode

The load function runs before the component is created or updated.

We store the current url in a variable and pass it to the PageTransition component as the url prop.

The PageTransition component will then destroy and recreate itself (transitioning out and back in) only when the user navigates to a new page.

Discussion (13)

Collapse
ptyork profile image
Paul York

Joined to (a) say thanks! and (b) provide some updates that I think are needed. At least for me with the latest SvelteKit builds.

First, during page load, I ASSUME the new page is inserted with opacity of 0 (instead of maybe "display: none") below the old page. This causes anything below the PageTransition component (footer in my case) to jump down and a scrollbar to appear or lengthen depending on the original page content. Pretty jarring. This didn't happen in an earlier build, so maybe it was a change in Svelte/SvelteKit somewhere?? Anyway, the fix was kinda tricky.

<script>
  import { fly } from 'svelte/transition';
  export let url = "";
</script>

<div class="transition-outer">
  {#key url}
    <div class="transition-inner"
        in:fly={{ y:-50, duration: 500, delay: 500 }}
        out:fly={{ y:50, duration: 500 }}>
      <slot />
    </div>
  {/key}
</div>

<style>
  .transition-outer {
    display: grid;
    grid-template: 1fr 1fr;
  }
  .transition-inner {
    grid-row: 1;
    grid-column: 1;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

I had to wrap the whole thing in a display-grid div with only a single cell and then force the content div into that cell. Bit of a hack, but it fixed the issue.

Second, definitely related to the change from page to url in the load function. The url is a URL object, not a string. At least in recent builds. SO, even when clicking on the same link (e.g., clicking on home while viewing home), the transition effect occurred since the {key} was detecting a changed object even though the underlying values remained unchanged.

ANYWAY, the fix there is easy. On the __layout page (or wherever), change the prop to be url.href. E.g.,:

  export const load = async ({ url }) => ({
    props: {
      url: url.href
    }
  });
Enter fullscreen mode Exit fullscreen mode

Now it's a plain string and behaves as expected.

Collapse
joshlucpoll profile image
Josh Pollard

Adding sveltekit:noscroll attribute to all <a> tags fixed the scrolling to top issue for me

<a href="path" sveltekit:noscroll>Path</a>

Collapse
glassforms profile image
Robert Stewart

Have you ever tried this approach using a fixed navigation? In all of the demos I've seen, when you click on a link, the site jumps you to the top of the page before transitioning the page out. Do you know why this happens, and how to prevent it?

Collapse
tomaaron profile image
Tom Koch

I'm having the same issue with bulma and fixed navbar. The only way to mitigate is to remove the out transition.

Collapse
evanwinter profile image
Evan Winter Author

I am seeing this too. Not sure how to fix at the moment.

I'm guessing it's because there's a moment mid-transition where the previous page content is unmounted and the new page content hasn't yet mounted, resulting in a document height that doesn't exceed the window height.

I know Gatsby and NextJS have solutions for persisting scroll position; I wonder if there's something like that out there for SvelteKit?

Collapse
felixbaumgaertner profile image
felixbaumgaertner

Thanks for this nice tutorial!
It seems like the variable names have changed, so that "export const load = async ({ page })" has to be changed to "export const load = async ({ url })". Don't forget the props: "key: page.path" to "key: url"

Collapse
evanwinter profile image
Evan Winter Author

Ah, thanks, I hadn't seen that! I'll update this post to reflect those changes

Collapse
tonborzenko profile image
Anton Borzenko • Edited on

Thank you for great job. Is it possible to use import { page } from '$app/stores', how do you think? I tried to use $page.path as a key, but it has a bug in animation between components...

Collapse
evanwinter profile image
Evan Winter Author • Edited on

What kind of bug? Is it changing to the next route before it finishes the "out" transition of the previous route?

If so... and I'm not sure I'll word this right... but essentially, I think the issue is that the $page.path variable is reactive and updates immediately when you navigate to the next route, irrespective of which render instance of the component it started with. With the approach shown in the post, we store a reference to the previous route that's scoped to the particular render instance, so PageTransition.svelte doesn't see a new key until after the first route has transitioned out (destroyed itself) – then, prior to transitioning back in (recreating itself), it loads the new key for the new route.

Collapse
darkeye123 profile image
Matej Leško

Thank you for this, I approached this by using $page store directly but basically new page didn't occur, I just saw transition out. This could explain the behavior

Collapse
ohaleks profile image
Aleksander Brymora

Perfectly done! And worked the first time!
Thanks for sharing

Collapse
nipodemos profile image
Nipodemos

thank you very much for this post, simple and functional

Collapse
evanwinter profile image
Evan Winter Author • Edited on

My pleasure! Glad it was helpful.