Tutorial hero
Lesson icon

Creating a Shared Element Transition Animation in Ionic (StencilJS)

Originally published August 13, 2019 Time 12 mins

Although you may or may not recognise the term shared element transition, you will likely have seen this pattern applied in many applications that you have used. It is typically applied in a master/detail type of situation, where we would select a particular element to reveal more information. The basic idea is that instead of just transitioning normally to a new page, or launching a modal, the selected element “morphs” into its position on the next page.

It will look something like this:

In this tutorial, we will be walking through how to implement this in an Ionic and StencilJS application.

Animations like this are nice because they add a bit of interest and intrigue to the application, but it also serves a purpose. This particular animation helps to reinforce the concept that what is being displayed on this new page is related to the element that was selected on the previous page. Although there is a bit of smoke and mirrors going on here, it appears that we are following the element on its journey to the new page, rather than there being a distinct “switching” of pages.

How exactly you might implement this pattern in your own application will depend a lot on the context, but this tutorial will provide an example and walk through the general concepts. I have written a tutorial previously on how to create a shared element transition for an Ionic/Angular application, so although I will still cover the basic concepts here, if you would like more context I would recommend reading that tutorial as well.

The Basic Concept

There are different ways you could go about implementing this, but the approach we will be taking is actually reasonably simple. The general idea goes like this:

  1. Fade in the detail page on top of the master page
  2. Have the “shared” element be positioned in the same spot on the detail page as it was on the master page
  3. As the detail page is fading in, animate the position of the shared element to its normal position for the detail page

Here is the exact same transition that we looked at above, but slowed down 10x:

It still might be a little difficult to tell what is going on here, so let’s break it down:

  1. The user clicks one of the cards
  2. The detail page starts fading in on top of the master page (nothing on the master page changes at all)
  3. Information containing the position of the image on the master page is passed to the detail page
  4. The header image on the detail page is initially set to appear in the same position as the image from the master page
  5. Once the detail page begins displaying, the header image starts to animate into its “normal” position on the detail page

For us to implement this process in an Ionic and StencilJS application we will need to:

  1. Launch a modal with a custom animation so that it fades in
  2. Use componentProps to pass the image position to the detail modal
  3. Using the componentDidLoad hook, set the initial position of the header image to match the position of the passed in data
  4. Animate the header image back to its regular position

Let’s start stepping through how to do that now.

1. Set up the Modal Controller

First, we will need to make sure that we have the <ion-modal-controller> web component set up in our application. We will just be adding this to the app-root component. If you are not familiar with this concept already, I would recommend watching Using Ionic Controllers with Web Components. This video also covers interacting with the modal controller and modal elements, which we will be doing later on in this tutorial.

Modify src/components/app-root/app-root.tsx to include the <ion-modal-controller>:

import { Component, h } from '@stencil/core';

@Component({
  tag: 'app-root',
  styleUrl: 'app-root.css',
})
export class AppRoot {
  render() {
    return (
      <ion-app>
        <ion-router useHash={false}>
          <ion-route url="/" component="app-home" />
        </ion-router>
        <ion-modal-controller />
        <ion-nav />
      </ion-app>
    );
  }
}

2. Create a Custom Modal Transition Animation

A key part of the shared element transition is having the detail page fade in on top of the master page. As I mentioned, we will be using a modal to overlay our detail page over the master page, but the default modal animation doesn’t use a “fade in” effect where the opacity is gradually animated from 0 to 1. Therefore, we are going to create our own custom modal animation so that we can make it do whatever we like.

We will define a custom animation in a separate file, and then pass that into the enterAnimation property for our modal. If you are not familiar with this and would like some additional context, I have another tutorial that explores this concept of creating custom animations in more depth: Create a Custom Modal Page Transition Animation in Ionic

Create a file at src/animations/fade-in.ts and add the following:

import { Animation } from '@ionic/core';

export function myFadeInAnimation(
  AnimationC: Animation,
  baseEl: HTMLElement
): Promise<Animation> {
  const baseAnimation = new AnimationC();

  const backdropAnimation = new AnimationC();
  backdropAnimation.addElement(baseEl.querySelector('ion-backdrop'));

  const wrapperAnimation = new AnimationC();
  wrapperAnimation.addElement(baseEl.querySelector('.modal-wrapper'));

  wrapperAnimation
    .beforeStyles({ opacity: 1 })
    .fromTo('translateX', '0%', '0%');

  backdropAnimation.fromTo('opacity', 0.01, 0.4);

  return Promise.resolve(
    baseAnimation
      .addElement(baseEl)
      .easing('cubic-bezier(0.36,0.66,0.04,1)')
      .duration(500)
      .beforeAddClass('show-modal')
      .add(backdropAnimation)
      .add(wrapperAnimation)
  );
}

3. Create the Modal and Pass the Position Information

Now we are going to define our “master” page. We are just going to use some dummy data to create a list of cards with images. What we need to happen is that when one of these images is clicked, we will launch a modal with our custom animation, and we will also pass some additional information regarding the position of the clicked image to the modal.

Modify src/components/app-home/app-home.tsx to reflect the following:

import { Component, State, h } from '@stencil/core';
import { myFadeInAnimation } from '../../animations/fade-in';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css',
})
export class AppHome {
  @State() cards = [1, 2, 3, 4, 5];

  async launchDetail(event) {
    const modalCtrl = document.querySelector('ion-modal-controller');

    let modal = await modalCtrl.create({
      component: 'app-detail',
      enterAnimation: myFadeInAnimation,
      componentProps: {
        coords: {
          x: event.target.x,
          y: event.target.y,
        },
      },
    });

    modal.present();
  }

  render() {
    return [
      <ion-header>
        <ion-toolbar color="light">
          <ion-title>Products</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content class="ion-padding">
        {this.cards.map(() => (
          <ion-card button onClick={(event) => this.launchDetail(event)}>
            <img src="/assets/grateful.jpg" />
          </ion-card>
        ))}
      </ion-content>,
    ];
  }
}

You can see in the code above that we are importing and using our myFadeInAnimation that we created, and we also pass in the x and y position of the clicked image as componentProps. We won’t actually need to make use of the x position in this tutorial, but depending on your circumstances, you may need to.

4. Animate the Element into Position

The next step is to define our “detail” page and to set the initial position of the image to the passed in y value. We will then need to animate the image from that initial position to its “normal” position.

To achieve the animation, we will be using the Web Animations API. Ionic is actually currently working on releasing their own animations API which will closely resemble the Web Animations API, but it will be a more stable option and optimised to work with Ionic. This will work for now, but I intend to swap it out later. There is no specific way that you need to perform this animation anyway, you can do it however you like (keeping performance in mind, of course).

Modify src/components/app-detail/app-detail.tsx to reflect the following:

import { Component, Element, h } from '@stencil/core';

@Component({
  tag: 'app-detail',
  styleUrl: 'app-detail.css',
})
export class AppDetail {
  @Element() el: HTMLElement;

  private modalElement: HTMLIonModalElement;

  componentDidLoad() {
    this.modalElement = this.el.closest('ion-modal');
    const y = this.modalElement.componentProps.coords.y;

    this.el
      .querySelector('.header-image')
      .animate(
        [
          { transform: `translate3d(0, ${y - 56}px, 0) scale3d(0.9, 0.9, 1)` },
          { transform: `translate3d(0, 0, 0) scale3d(1, 1, 1)` },
        ],
        {
          duration: 500,
          easing: 'ease-in-out',
        }
      );
  }

  close() {
    this.modalElement.dismiss();
  }

  render() {
    return [
      <ion-header>
        <ion-toolbar color="light">
          <ion-title>Detail</ion-title>
          <ion-buttons slot="end">
            <ion-button onClick={() => this.close()}>
              <ion-icon slot="icon-only" name="close" />
            </ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>,

      <ion-content>
        <img class="header-image" src="/assets/grateful.jpg" />

        <div style={{ padding: `20px` }} class="container">
          <h2>Really cool...</h2>

          <p>
            Lorem Ipsum is simply dummy text of the printing and typesetting
            industry. Lorem Ipsum has been the industry's standard dummy text
            ever since the 1500s, when an unknown printer took a galley of type
            and scrambled it to make a type specimen book.
          </p>
          <p>
            Lorem Ipsum is simply dummy text of the printing and typesetting
            industry. Lorem Ipsum has been the industry's standard dummy text
            ever since the 1500s, when an unknown printer took a galley of type
            and scrambled it to make a type specimen book.
          </p>
        </div>
      </ion-content>,
    ];
  }
}

We get the position information from the modal inside of the componentDidLoad hook, and then we use that when defining the animation on the element with the header-image class.

We animate from these properties:

{ transform: `translate3d(0, ${y - 56}px, 0) scale3d(0.9, 0.9, 1)` },

which will move the header image down into a y position that matches the passed in y value (the 56px here accounts for the header), and we also shrink it in size a bit by scaling. Then we animate it to these properties:

{
  transform: `translate3d(0, 0, 0) scale3d(1, 1, 1)`;
}

which are just the defaults, so it will go back to its normal position at the top of the page.

Summary

Although there is a bit of set up work involved here, this animation is rather effective and it certainly makes the application look more impressive. Since we are just using the transform and scale properties, this also means that the animation should perform well.

As I mentioned, the exact implementation is going to depend on how you want your application to look and where the images need to move from and to, but this tutorial should serve well to explain the basic concepts, and you can modify it to suit your needs.

Learn to build modern Angular apps with my course