Tutorial hero
Lesson icon

Create a Custom Modal Page Transition Animation in Ionic

Originally published October 31, 2018 Time 10 mins

When we launch a modal in an Ionic application, an animation is used to provide a smooth transition as it appears on the screen. The default animation for iOS and Android causes the modal to slide up from the bottom – the effect is more pronounced on iOS, but the animation is similar on both platforms. This animation is perfectly suited to the platforms it runs on and most people will just stick with the default animation (there is no specific reason not to).

However, it is possible to supply your own custom transition animations if you wish. In this tutorial, we are going to look at how we can modify the default animations that Ionic supplies to create our own open and close animations for modals.

We will be creating some custom animations that look like this:

Modal Slide Out Animation in Ionic Modal Pop in Animation in Ionic

Before We Get Started

Last updated for Ionic 4, beta.13

This is an advanced tutorial that assumes you already have a decent working knowledge of the Ionic framework. If you require more introductory level content on Ionic I would recommend checking out my book or the Ionic tutorials on my website.

1. Supplying an Animation to a Modal

The way in which we typically create a modal looks something like this:

this.modalCtrl
  .create({
    component: ModalPage,
  })
  .then((modal) => {
    modal.present();
  });

However, there are many more options that we can supply when creating this modal which are outlined here. Those options include an option to specify an enterAnimation and a leaveAnimation like this:

this.modalCtrl
  .create({
    component: ModalPage,
    enterAnimation: myEnterAnimation,
    leaveAnimation: myLeaveAnimation,
  })
  .then((modal) => {
    modal.present();
  });

All we need to do is create those myEnterAnimation and myLeaveAnimation animations. That is what we will be focusing on in the next step.

2. Creating a Custom Transition Animation

Creating the transition animation isn’t exactly an easy task, fortunately, the Ionic team have already done most of the work for us. We can use the existing animations that they have created for the default transitions and build our solution on top of that.

You can find those animations here. Our solution is going to look slightly different to this, but most of the code will remain the same. We are going to create new files for our animations, and those files are going to export the animation we want to use.

Create a new file at src/app/animations/enter.ts and add the following:

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

export function myEnterAnimation(
  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('translateY', '100%', '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(400)
      .beforeAddClass('show-modal')
      .add(backdropAnimation)
      .add(wrapperAnimation)
  );
}

We have pretty much just copy and pasted the code from Ionic directly in here, except that we are now importing Animation from @ionic/core.

This code might look somewhat intimidating, but there are only a few things you need to know for basic animations. The most important part is the wrapperAnimation:

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

The fromTo method animates the styles on the modal from one thing to another thing. In this case, the translateY is being animated from 100% (i.e. off-screen, to the bottom) to 0% (i.e. in the center of the screen). You can change the properties being animated here, and you can event animate multiple properties by chaining the fromTo method:

wrapperAnimation
  .beforeStyles({ opacity: 1 })
  .fromTo('translateY', '100%', '0%')
  .fromTo('somethingelse', 'beforevalue', 'aftervalue')
  .fromTo('somethingelse', 'beforevalue', 'aftervalue');

You may also wish to modify the duration to change the length of the animation of the easing function. More complex animations may require more modifications, but you should be able to do a lot with just this.

Let’s also set up the leave animation before continuing:

Create a new file at src/app/animations/leave.ts and add the following:

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

export function myLeaveAnimation(AnimationC: Animation, baseEl: HTMLElement): Promise<Animation> {

    const baseAnimation = new AnimationC();

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

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

    wrapperAnimation.beforeStyles({ 'opacity': 1 })
                    .fromTo('translateY', '0%', `${window.innerHeight - wrapperElRect.top}px`);

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

    return Promise.resolve(baseAnimation
      .addElement(baseEl)
      .easing('ease-out')
      .duration(250)
      .add(backdropAnimation)
      .add(wrapperAnimation));

}

We could actually make use of these animations with our modal now and they would work. That’s a bit boring though because the animation is exactly the same as the default one.

Instead, we are going to create a transition animation that will slide the modal in from the left, and out through the right.

This is an easy modification to make because it is mostly the same as the original animation, just in a different direction.

Modify src/app/animations/enter.ts to reflect the following:

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

export function myEnterAnimation(
  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', '-100%', '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(400)
      .beforeAddClass('show-modal')
      .add(backdropAnimation)
      .add(wrapperAnimation)
  );
}

Modify src/app/animations/leave.ts to reflect the following:

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

export function myLeaveAnimation(AnimationC: Animation, baseEl: HTMLElement): Promise<Animation> {

    const baseAnimation = new AnimationC();

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

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

    wrapperAnimation.beforeStyles({ 'opacity': 1 })
                    .fromTo('translateX', '0%', `${window.innerWidth - wrapperElRect.left}px`);

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

    return Promise.resolve(baseAnimation
      .addElement(baseEl)
      .easing('ease-out')
      .duration(250)
      .add(backdropAnimation)
      .add(wrapperAnimation));

}

The original animation was modifying translateY to push the modal up and down, but now we are using translateX to push the modal left and right. It is the exact same concept, except we are animating over the horizontal plane rather than the vertical plane. Also, keep in mind that we have changed the to value in the leave animation to use the innerWidth and left values instead of innerHeight and top.

3. Using a Custom Transition for a Modal

We have our custom animation, now we just need to use it. Fortunately, this is extremely straight-forward. Just import the animation into the page where you are creating the modal:

import { myEnterAnimation } from '../animations/enter';
import { myLeaveAnimation } from '../animations/leave';

and then supply those animations when creating the modal:

this.modalCtrl
  .create({
    component: ModalPage,
    enterAnimation: myEnterAnimation,
    leaveAnimation: myLeaveAnimation,
  })
  .then((modal) => {
    modal.present();
  });

4. Getting Fancier

The animation we created was pretty simple, we mostly kept the code exactly the same but we changed the direction. Now we are going to create something a little more custom:

Modal Pop in Animation in Ionic

Modify src/app/animations/enter.ts to reflect the following:

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

export function myEnterAnimation(
  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
    .fromTo(
      'transform',
      'scaleX(0.1) scaleY(0.1)',
      'translateX(0%) scaleX(1) scaleY(1)'
    )
    .fromTo('opacity', 0, 1);

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

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

Modify src/app/animations/leave.ts to reflect the following:

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

export function myLeaveAnimation(AnimationC: Animation, baseEl: HTMLElement): Promise<Animation> {

    const baseAnimation = new AnimationC();

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

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

    wrapperAnimation
      .fromTo('transform', 'scaleX(1) scaleY(1)', 'scaleX(0.1) scaleY(0.1)')
      .fromTo('opacity', 1, 0);

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

    return Promise.resolve(baseAnimation
      .addElement(baseEl)
      .easing('ease-out')
      .duration(400)
      .add(backdropAnimation)
      .add(wrapperAnimation));

}

Now we have an animation that will cause the modal to “pop” in and out of the screen. We have removed the beforeStyles entirely as we don’t want the modal to be visible to begin with. Instead of supplying an initial opacity, we supply a second fromTo that will animate the opacity of the modal from 0 to 1 over the course of the animation.

Rather than moving the modal on and off the screen with translate, we are now using a scale transform to shrink and grow the modal. This will cause the modal to animate from a very small size to its natural size, and then from it’s natural size to a very small size as it disappears from the changing opacity. We do still need to keep the translateX on the enter animation as well to position the modal correctly.

We’ve also changed the duration of the animation to be 400ms, which is slightly longer than the other animation.

Summary

With just a few small changes, we are able to significantly impact the way the modal is animated onto and off of the screen. It is probably not all that often that you would need to use your own custom transition animations, but for circumstances where you do, this is a rather straight-forward and powerful way to do that.

Learn to build modern Angular apps with my course