DEV Community

Cover image for Instagram-like page transitions with Svelte
Adrien Denat
Adrien Denat

Posted on • Edited on

Instagram-like page transitions with Svelte

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 that Chrome is proposing.
But until that, we still have a little more work to do!

Using my portfolio 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:

<div in:fn={params} out:fn={params} />
Enter fullscreen mode Exit fullscreen mode

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 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:

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

...

  {#key $page.params.story}
    <div in:fn={params} out:fn={params} />
  {/key}
Enter fullscreen mode Exit fullscreen mode

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:

<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} />
Enter fullscreen mode Exit fullscreen mode

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

adriendenat.com stories swipe transition

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.

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:

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 />
Enter fullscreen mode Exit fullscreen mode

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

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:

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;` 
    : ''} 
/>
Enter fullscreen mode Exit fullscreen mode

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).

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.

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!

Top comments (0)