February 13, 2019

How to Apply Nested Transitions in Vue

Animating an element when showing or hiding it is pretty easy in Vue — you just have to wrap the element with the <transition> component.

But what about those cases when you want to show or hide nested children sequentially? For example, after the root element is shown, show element A, and after that, show element B, and so on.

This is still an easy thing to do in Vue; you just need a way to know when the previous transition is done to start the next one.

If you haven’t done it before, and you’re wondering how, I’m going to save you some time and show you how to do it in a clean, controllable way. But before that, take a look at this CodePen to see what we’re going to build:

As you can see in the demo above, we’re going to create a simple modal box that’s displayed in two steps (transitions). First, we show the overlay background, and then we show the white content box.

I’ll break the tutorial into three sections. First, we’ll create the button and the modal box. The user can show the modal box by clicking on the button and close it by clicking on the overlay background. In this section, the modal will open without animations.

In the second section, we’ll add a single-step transition — so the overlay background and the content box will be shown simultaneously.

And in the final section, we’ll add a nested transition for the content box — which will be shown after the overlay background transition is done.

Showing the modal box without animation

Let’s start things quickly with Vue CLI 3’s instant prototyping. So create App.vue, and put the following into the <template> section:

<template>
  <div id="app">
    <modal v-if="showModal" @close="showModal = false" />
    <button class="button" @click="showModal = true">Show Modal</button>
  </div>
</template>

We have here a button that sets showModal to true. And when it’s true, we display the <modal> component, as shown above. (Note that we haven’t created that component yet, but we will shortly.)

Also, note how we set showModal to false when <modal> emits close custom event.

Now, in the <script> section, put this:

<script>
import Modal from './Modal'

export default {
  components: { Modal },
  data () {
    return {
      showModal: false
    }
  }
}
</script>

And then this into <style>:

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  height: 100vh;
  width: 100vw;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.button {
  border-radius: 2px;
  background: #D55672;
  border: none;
  padding: 10px;
  font-size: 14px;
  font-weight: bold;
  cursor: pointer;
  color: #FFF;
  outline: none;
  transition: 0.1s background;
}

.button:hover {
  background: #AA445B;
}
</style>

Next, let’s create the Modal.vue component, then add the following into the template section:

<template>
  <div class="modal" @click="$emit('close')">
    <div class="content" @click.stop>
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quisquam,
      placeat, unde! Architecto laboriosam ducimus atque cum dolore doloribus
      obcaecati vero. Minus porro sapiente unde fuga incidunt quidem
      necessitatibus mollitia libero?
    </div>
  </div>
</template>

Note that the root element here (.modal) is used as the overlay background. When the user clicks on it, it emits the close event.

Also, note how we’re using @click.stop on .content to prevent it from closing the modal when it’s clicked on.

The <script> section should be empty for now:

<script>export default {}</script>

Next, add this for the styling:

<style scoped>
.modal {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0,0,0,0.6)
}

.content {
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(100% - 20px);
  max-width: 500px;
  transform: translate(-50%, -50%);
  background: #FFF;
  border-radius: 3px;
  padding: 20px;
  line-height: 1.5;
  font-size: 18px;
  color: #444;
  box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
</style>

At this point, you should be able to open/close the modal box, but without animation.

Single-step transition

Now, let’s make the modal box open with a single-step fade transition.

It’s so easy to do. Just wrap the content of the modal component with <transition name="fade"></transition>, like this:

<template>
  <transition name="fade">
    <div class="modal" @click="$emit('close')">
      <div class="content" @click.stop>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quisquam,
        placeat, unde! Architecto laboriosam ducimus atque cum dolore doloribus
        obcaecati vero. Minus porro sapiente unde fuga incidunt quidem
        necessitatibus mollitia libero?
      </div>
    </div>
  </transition>
</template>

Then, define the fade transition in the <style> section, like this:

.fade-enter,
.fade-leave-to {
  opacity: 0;
}

.fade-enter-active,
.fade-leave-active {
  transition: 0.2s opacity ease-out;
}

That’s it! Now your single-step transition should work as expected.

Applying nested transitions to the modal

Here’s how we’re going to do it:

  1. Wrap .content with <transition name="fade"> so it can be animated.
  2. Add v-if="showContent" to .content so we can specify when that element can be shown (we can do that by setting showContent to true). Also, note that we have to define showContent in the modal data() object.
  3. Listen for @after-enter on the root <transition> component. And when that event fires, we set showContent to true.
  4. Modify @click of .modal to set showContent to false. So, instead of emitting close event immediately on click, we hide the .content element with animation, and only after that animation is done, will we emit the close event. So this leads us to our next point.
  5. Add @after-leave="$emit('close')" to .content <transition> component.

After applying the above steps, the modal’s <template> should become like this:

<template>
  <transition name="fade" @after-enter="showContent = true">
    <div class="modal" @click="showContent = false">
      <transition name="fade" @after-leave="$emit('close')">
        <div v-if="showContent" class="content" @click.stop>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quisquam,
          placeat, unde! Architecto laboriosam ducimus atque cum dolore
          doloribus obcaecati vero. Minus porro sapiente unde fuga incidunt
          quidem necessitatibus mollitia libero?
        </div>
      </transition>
    </div>
  </transition>
</template>

And let’s not forget to add showContent to data() object:

<script>
export default {
  data () {
    return {
      showContent: false
    }
  }
}
</script>

So, here’s how the showing part works: When the user clicks on the button, we set showModal to true, and that triggers the root <transition>, which only shows the overlay background alone. After that transition is done, after-enter is fired. And on that event, we set showContent to true to start the transition for that element.

Now for the hiding part, when the user clicks on the overlay background, we set showContent to false, which runs the leaving transition for the .content element. And when that transition is done, it fires the after-leave event. We handle that event by emitting the close event which sets showModal to false to hide modal box with transition.

If you now run the example, you should see the nested transitions working as expected.

Stay up-to-date on the latest projects and articles from me