DEV Community

Cover image for Svelte Smooth page transitions
Giorgos Kontopoulos 👀
Giorgos Kontopoulos 👀

Posted on • Updated on

Svelte Smooth page transitions

Demo: https://amazing-kirch-8cb3f5.netlify.app/

I have tried before and failed to make my page transtitions implemented in a way that we don't have to import the pageTransition component in each and every route.

I have finally created a pageTransition component that can just be dropped in the $layout.svelte component (_layout.svelte in Sapper) and then be used by all existing or future routes.

I am going to show you here the steps and the thought process for setting up the component and the changes along the way which was the natural steps that I evolved this component and hopefully they can help someone in understanding layout/route based Svelte reactivity better.

There is also a github repo that I have committed all different phases if you need to know all the details. The tutorial is mostly focuses on inexperienced developers. For more experienced developers you can just skip to the last sections or go straight to the repo.

I have chosen SvelteKit for this demonstration just to try it out and mostly because it seems to be the future of Svelte. If you are using Sapper the steps should be almost identical.

Setup Svelte@next

Inside an empty project directory run

npm init svelte@next
pnpm install
pnpm run dev
Enter fullscreen mode Exit fullscreen mode

NOTE: Feel free to use npm where I use pnpm. The two have exactly the same syntax.

After that you can browse to localhost:3000 and be presented with the demo route.

Setup a 2nd route a Simple Navigation component and a $layout component

Setup minimum routes and components to be able to navigate from one page to another.

<!-- src/components/Nav.svelte -->
<div>
  <a href="/">Home</a>
  <a href="/about">About</a>
</div>
Enter fullscreen mode Exit fullscreen mode
<!-- src/routes/about.svelte -->
<main>
  <h1>About Page</h1>
  <p>This is the about page</p>
  <p>More paragraphs of the about page</p>
</main>
Enter fullscreen mode Exit fullscreen mode
<!-- src/routes/$layout.svelte -->
<script>
  import Nav from '../components/Nav';
</script>

<Nav/>

<slot/>
Enter fullscreen mode Exit fullscreen mode

We should have now the two routes and be able to navigate between Home page and About page.

The beauty of SvelteKit (the magic of snowpack) is that you should be able to see changes in your browser immediately after saving the files.

Consistent styling of pages by creating global.css

Some stylistic changes are due to make the styles consistent across all pages.

We take all styles from index.svelte and add them to a new file static/global.css

/* static/global.css */
:root {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

main {
  text-align: center;
  padding: 1em;
  margin: 0 auto;
}

h1 {
  color: #ff3e00;
  text-transform: uppercase;
  font-size: 4rem;
  font-weight: 100;
  line-height: 1.1;
  margin: 4rem auto;
  max-width: 14rem;
}

p {
  max-width: 14rem;
  margin: 2rem auto;
  line-height: 1.35;
}

@media (min-width: 480px) {
  h1 {
    max-width: none;
  }

  p {
    max-width: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

Link css file from app.html by adding following line before %svelte.head%

<!-- add to src/app.html -->
    <link rel="stylesheet" href="global.css">
Enter fullscreen mode Exit fullscreen mode

Simple PageTransitions component

Here is a simple PageTranstitions svelte component using svelte/transition package.

<!-- src/component/PageTransitions.svelte -->
<script>
  import { fly } from 'svelte/transition';
</script>

  <div
    in:fly="{{ y: -50, duration: 250, delay: 300 }}"
    out:fly="{{ y: -50, duration: 250 }}" 
    >
    <slot/>
  </div>
Enter fullscreen mode Exit fullscreen mode

It makes the page fly in and out from top of the viewport. We delay a bit on the in transition to avoid the in and out transitions from working at the same time.

Perhaps if anyone know how to add crossfade to it please do let me know in the comments.

If you add it as a wrapper on any route it does work as expected and it is the way I have seen all tutorials use a page transition component. There might also be use cases where we only need it on certain routes and than this would be the way to use it.

<!-- src/routes/about.svelte -->
<script>
    import Counter from '$components/Counter.svelte';
    import PageTransitions from '../components/PageTransitions.svelte';
</script>

<PageTransitions>
  <main>
    <h1>About Page</h1>
    <p>This is the about page</p>
    <p>More paragraphs of the about page</p>
  </main>
</PageTransitions>
Enter fullscreen mode Exit fullscreen mode

More versatile PageTransitions component

For most use cases though I believe setting PageTransitions in the $layout.svelte component and than having all routes use it is the way to go as it we set it and forget it.

First lets remove the component from all routes and bring them to their initial state (setup setps a little above).

If we just put the PageTransitions component in $layout.svelte component without any modifications though we will not be able to see any transitions.

<!-- src/routes/$layout.svelte -->
<script>
  import Nav from '../components/Nav';
  import PageTransitions from '../components/PageTransitions';
</script>

<Nav/>

<PageTransitions>
  <slot/>
</PageTransitions>
Enter fullscreen mode Exit fullscreen mode

NOTE: I noticed that SvelteKit is not loading all changes automatically to the browser. Perhaps this is something that will be fixed in later versions so lets not focus on it just keep in mind to refresh the browser when you can't see any changes.

The problem is that the component is not reactive as is. We should make it reactive to the change of the route.

Reactive Nav component

Lets first make the Nav component reactive by underlying the current route link.

<!-- src/components/Nav.svelte -->
<script>
    export let segment;
</script>

<style>
  a {
    text-decoration: none;
  }
  .current {
    text-decoration: underline;
  }
</style>
<div>
  <a href="/" class='{segment === "/" ? "current" : ""}'>Home</a>
  <a href="/about" class='{segment === "/about" ? "current" : ""}'>About</a>
</div>
Enter fullscreen mode Exit fullscreen mode

Pass the $page.path as segment variable to $layout.svelte to make the Nav reactive.

<!-- src/routes/$layout.svelte -->
<script>
  import Nav from '../components/Nav';
  import { page } from '@sveltejs/kit/assets/runtime/app/stores.js'
</script>

<Nav segment={$page.path}/>

<slot/>
Enter fullscreen mode Exit fullscreen mode

Now if you change the Nav links the current route link should be underlined.

Reactive PageTransition component

We now need to make the component reactive by creating a refresh prop and using key directive which means that when the key changes, svelte removes the component and adds a new one, therefore triggering the transition.

<!-- src/component/PageTransitions.svelte -->
<script>
  import { fly } from 'svelte/transition';
  export let refresh = '';
</script>

{#key refresh}
  <div
    in:fly="{{ y: -50, duration: 250, delay: 300 }}"
    out:fly="{{ y: -50, duration: 250 }}" 
    >
    <slot/>
  </div>
{/key}
Enter fullscreen mode Exit fullscreen mode

Also lets pass the $page.path variable to PageTransition component similar to how we did it with the Nav component.

<!-- src/routes/$layout.svelte -->
<script>
  import Nav from '../components/Nav';
  import PageTransitions from '../components/PageTransitions';
  import { page } from '@sveltejs/kit/assets/runtime/app/stores.js'
</script>

<Nav segment={$page.path}/>

<PageTransitions refresh={$page.path}>
  <slot/>
</PageTransitions>
Enter fullscreen mode Exit fullscreen mode

You should now have your smooth page transtitions without needing to add the component to every route.

Improving the page transitions

When the pages are longer in height the transition are not actually that smooth because it might cause the scrollbars to flicker creating an undesired moving effect on the contents of the page.

For understanding the problem lets force each page to be a little bigger in height

/* add in static/global.css */
main {
  min-height: 600px;
}
Enter fullscreen mode Exit fullscreen mode

Depending on your viewport size you might need to adjust the height so that there is no scrollbars on when not transitioning (idle state).

If you refresh your browser and navigate from page to page you should see the flickering effect.

To mitigate this problem lets add the following css to your global.css.

/* add in static/global.css */
body {
  overflow-y: scroll;
}
Enter fullscreen mode Exit fullscreen mode

And now the scrollbars will be visible at all times but might not always have the scroll handle to drap up/down. This will prevent them from appearing and disappearing and eliminates the problem.

Another alternative would be to use

/* add in static/global.css */
html { 
  margin-left: calc(100vw - 100%); } 
}
Enter fullscreen mode Exit fullscreen mode

Read more about the flickering scrollbar and the above solutions on css-tricks

Conclusion

Making a component reactive on the layout/routing level is easy enough as seen above. This technique can be used with any component we include in layout or any component that we want to be reactive when the route changes.

Please post your thoughts on the comments below. I would be happy to improve on the above code if something is not considered right or best practice and I do welcome corrections.

If you liked this tutorial or found it useful please share or like or give me a star on github. Any of the above gives me an incentive to dig deeper and write more useful tutorials.

Top comments (28)

Collapse
 
bfanger profile image
Bob Fanger

I've created a REPL which shows how to use a crossfade between pages. svelte.dev/repl/0ad58a0d830f4001b9...

Hint: Use the same instance of the crossfade by creating the send and receive in a separate javascript file.

Collapse
 
kbuffington profile image
kbuffington

This was exactly the demo I needed, and it easily works to move around multiple UI elements at the same time. So cool!

svelte.dev/repl/b2a5fbb5ec86486bb7...

Collapse
 
skwasha profile image
Sascha Linn

I'm not sure in which version it happened... but the segment prop no longer appears to be valid. I've yet to find what the appropriate replacement is.

Collapse
 
giorgosk profile image
Giorgos Kontopoulos 👀 • Edited

@skwasha I have updated the repo and replaced segment with $page.path and updated also the tutorial to reflect this change. Let me know if this works for you.

Collapse
 
cholasimmons profile image
Chola • Edited

Hi @giorgosk, Great tutorial although my page fades are not ideal.

P.S: $page.path seems to have changed to $page.route.id?

Collapse
 
skwasha profile image
Sascha Linn • Edited

Hey, that's great! Thanks for the update. Where did you happen to find the location for the page stores? I tried searching and checking old messages on the Discord but never saw any mention of it. Do you know if that's going to be where these "built in" stores will be located moving forward with Svelte Kit?

thanks again.

Thread Thread
 
giorgosk profile image
Giorgos Kontopoulos 👀

@skwasha I started digging in the sveltekit node_modules/sveltejs/kit/assets/runtime/app/stores.js code looking for $page.path mentioned in the github.com/sveltejs/sapper/issues/824 and found it. Perhaps the import import { page } from '@sveltejs/kit/assets/runtime/app/stores.js'; will get more elegant when sveltekit gets released (or I might be missing the correct way to do it) but this will be the way to use it again according to the issue/824.

Thread Thread
 
sswam profile image
Sam Watkins • Edited

I searched the discord and found the right way to do it:

import { page } from '$app/stores';

Thanks for this article, by the way, it's helping me get off the ground with svelte/kit.

Another idea for a change, you can set the class="current" like this, it's a bit shorter:

<a href="/" class:current={segment==="/"}>Home</a>
<a href="/about" class:current={segment==="/about"}>About</a>

Collapse
 
giorgosk profile image
Giorgos Kontopoulos 👀 • Edited

Yes there is some talks about removing segment from sapper but nothing has been committed yet. In here github.com/sveltejs/sapper/issues/824 you can find the discussion and some alternatives (using $page store)

Collapse
 
skwasha profile image
Sascha Linn • Edited

Yes. I saw that discussion as well. I guess my point was that the above code is no longer functional (at least the bits that rely on segment). Somewhere along the way that proposed change seems to have made it into the current sveletkit branch.

Does anyone know where the page store is in the Sveltekit world? Previously, it was in @sapper/app.

Thread Thread
 
sswam profile image
Sam Watkins

import { page } from '$app/stores';

Collapse
 
delanyoyoko profile image
delanyo agbenyo

Great. But currently in sveltekit, you can just use $page.url.pathname to get the current page's path for the key in the PageTransition component. So there should no be any need to export the refrech prop.

Collapse
 
cholasimmons profile image
Chola

it's now $page.route.id

Collapse
 
mightyphoenix_1 profile image
Rakshit Kumar

Good read!
I think it would have been much better with short video clips of how the transitions are working with the changes.

Collapse
 
giorgosk profile image
Giorgos Kontopoulos 👀

Sorry I don't understand why would you need a video when there is a live site demo amazing-kirch-8cb3f5.netlify.app/ ?

Collapse
 
mightyphoenix_1 profile image
Rakshit Kumar

No, I meant the difference in the transitions before and after making changes for improvement.

Thread Thread
 
giorgosk profile image
Giorgos Kontopoulos 👀 • Edited

Ok I see perhaps I can do it when I get a chance. In the meantime if you need to see the improvements you can toggle the last css changes on and off on your locally installed repo.

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
 
ptyork profile image
Paul York

Late reply, but I don't think it has anything to do with a fixed nav. I think it's just invoking a transition from a scroll position other than the top of the page. At least I think I eliminated all of the variables and it was still happening. My guess is that the transition code in Svelte simply wasn't intended for content the size of a whole page and does something to trigger the browser to jump. Couldn't find it in their code, but I didn't look too hard. Anyway, the only solution I found was simply to remove the out: transition altogether. Just do an in:fade. Looks okay. Better than the "cut" default.

Collapse
 
joshua1 profile image
joshua Oguche

@giorgosk did you manage to figure out how to handle routing from code ? in sapper for example you have the "goto" method for routing. i couldn't find this feature in sveltekit

Collapse
 
giorgosk profile image
Giorgos Kontopoulos 👀

Sorry not done any further research on it

Collapse
 
ladvace profile image
Gianmarco

If I try to use PageTransition in the layout instead of the component I get the following error, do you know why?

Error: failed to load module for ssr: /Users/userName/Documents/GitRepos/project/src/lib/components/PageTransitions.svelt

Collapse
 
zofiaifoz profile image
zofiaifoz

Thanks a lot for your great tutorial. In my project I am using Svelte + GSAP and I would also like to use page transitions. I implemented your code, but GSAP animations don't work then. Would you have any advice on how to fix it?

Here is a demo:

Without Page Transitions:
stackblitz.com/edit/sveltejs-gsap-...

With Page Transitions:
stackblitz.com/edit/sveltejs-gsap-...

I would appreciate any help. Thank you!

Collapse
 
dansvel profile image
dan

dang, this is what i need,,

cool post,, thanks for the idea,,

Collapse
 
pavloskl profile image
pavloskl

Great article Giorgos!! Congrats.

Collapse
 
giorgosk profile image
Giorgos Kontopoulos 👀

Glad you liked it Pavlos, comments are always appreciated !!!