Instagram-like page transitions with Svelte
Tags: svelte, design, javascript, sveltekit
Published: 2022-10-27 10:29:44

Page transitions are one of the most complex things to do on the web and there is a good reason for it. There are many things involved when navigating from one page to another: mounting the new page, dismounting only once the transition is finished, handling shared states, updating the URL, handling browser navigation, etc...

Frameworks have been trying to solve this for years but it is still a challenge today. The good news is that soon we might be able to do it much more easily thanks to the [Shared Element Transition API](https://developer.chrome.com/blog/shared-element-transitions-for-spas/) that Chrome is proposing.
But until that, we still have a little more work to do!

Using my [portfolio](https://adriendenat.com/) as an example, let's see how well Svelte can do page transitions!

## The brief

One of the things that made the success of the Instagram mobile app is its great UX. In particular the "stories". It's been replicated everywhere, Messenger, WhatsApp, Google Photos, etc. The way the stories open, close and move from previous/next makes the navigation smooth and natural. 

Let's have a look at the animations we want to reproduce:

- The story opens up when I click on its thumbnail.
- I can swipe or click the story to go to the next/previous.
- I can drag down the story to go back to the homepage.

So how can we create an Instagram-like experience on our website using Svelte?

## The Svelte and SvelteKit toolkit

Svelte provides special animation properties that can be applied to elements to define their behavior on mount and dismount:

```html
<div in:fn={params} out:fn={params} />
```

Applying the `out` prop will wait until `fn` is executed before removing the element from the DOM.
The strength of Svelte animations toolkit is that the function `fn` used for animation can be a custom function that gives you great control over the transition.

With SvelteKit, the `in` and `out` animations are called when the component mount/dismount or when a [Svelte key](https://svelte.dev/docs#template-syntax-key) changes. 
So if there is a URL change but the component stays the same (for example if we go from /stories/1 to /stories/2, the URL changed but the component to show both pages is the same), we need to use a `key`:

```js
  import { page } from "$app/stores";

...

  {#key $page.params.story}
	<div in:fn={params} out:fn={params} />
  {/key}
```

## Different animations depending on navigation

In our case we want the "story" to animate differently depending on where we navigate to:
- "slide left/right" if we go next/previous
- "scale in/out" if we go in or out of a story

Thanks to the custom Svelte animation function we can handle all those scenarios:

```js
<script>
$: transitionsConfig = [
  {
    condition: c => c.toHomepage,
    transition: customScale
  },
  {
    condition: c => c.toHigherIndex,
    transition: customFly,
    inParams: { x: width, duration: 400, opacity: 1 },
    outParams: { x: -width, duration: 400, opacity: 1 }
  },
  {
    condition: c => c.toLowerIndex,
    transition: customFly,
    inParams: { x: -width, duration: 400, opacity: 1 },
    outParams: { x: width, duration: 400, opacity: 1 }
  }
];

$: config = transitionsConfig.find(({ condition }) => {
  const { from, to } = $navigating;

  const fromIndex = parseInt(from.params.story);
  const toIndex = parseInt(to.params.story);

  // If there is no story index in the pathname...
  if (!toIndex) {
    // ...it means we are going to the homepage: "/".
    return condition({
      toHomepage: true
    });
  }

  return condition({
    toHigherIndex: fromIndex < toIndex,
    toLowerIndex: fromIndex > toIndex
  });
});

$: ({ transition, inParams, outParams } = config);

</script>

<div in:transition={inParams} out:transition={outParams} />
```

This way you can "resolve" the right transition for the user navigation.

![adriendenat.com stories swipe transition](https://i.postimg.cc/qrj7k6BJ/slide-transition.webp)
<figcaption>Here is `customFly` in action, which is just the default `fly` from Svelte but with the custom parameters to animate left/right based on the route.</figcaption>



## Custom Svelte navigation transition

Now one of the coolest transitions on Instagram is how the stories are "vacuumed" back to their avatar when exited. Let's see if we can reproduce that!

First, we will need to know the screen position where to "vacuum" the story when it closes. To do that we have to cheat a bit because when the story closes, the main screen is not mounted, so we can't easily find the story thumbnail position.

For now, we will take the index of the current story (they are displayed in order) and with that, we can suppose its position on the main screen. 
Finally, we can scale out the story when it's exited to mimic the vacuum effect, using Svelte custom transition function. Here is a simplified version of it:

```js
import { scale } from "svelte/transition";

const customScale = (node, options) => {
	const storyIndex = stories[$page.params.story].index;
	const storyThumbnailPosition = { x: 50 * storyIndex, y: 100 };

	node.style.setProperty('transform-origin', `${storyThumbnailPosition.x}px ${storyThumbnailPosition.y}px`);
	node.style.setProperty('z-index', 1);
	return scale(node, options);
};

<Story transition:customScale />
```

Here we apply a CSS `transform-origin` with our calculated animation position to the element so the Svelte `scale` can scale out to the original story avatar position.

![Animation from adriendenat.com of a story opening and closing](https://i.postimg.cc/BJnj7HMX/closing-transition.webp)

Another trick to make this look like a real app is the "bounce" effect on the story thumbnail when the story closes. This is possible because SvelteKit gives us the really handy `$navigating.from`:

```js
let isActive = $navigating.from.url.pathname === storyPath;
let tween = spring(1, {
  stiffness: 0.03,
  damping: 0.15,
  precision: 0.001
});
$: isActive && tween.set(0);

...

<img 
  src={imgSrc} 
  style={isActive
    ? `transform: translate3d(${$tween * -140}px, ${$tween * 120}px, 0px) scale(${$tween * 3 + 1}); transition: none;` 
    : ''} 
/>
```

By the way, this is something particularly complex to do in the React world! (Next.js or Gatsby routers don't include a "from").

With this the `StoryButton.svelte` can know that we are navigating from its corresponding story, so we can animate it.

At this point, you probably see how we can achieve most of the animations we need!

## Code examples

I prepared a simplified version of the app on Codesandbox, check it out! 
> Notice that the drag navigation only works on mobile or (using the browser dev tools in responsive mode).

{% codesandbox 80lq7b %}

You can also find the source code of my portfolio (where the gifs are taken from) with a more complete implementation on Github [https://github.com/Grsmto/grsmto.github.io](https://github.com/Grsmto/grsmto.github.io).

Do you know any other tricks I could use to make this website look even more like a native app? Let me know in the comments!