How to Apply Nested Transitions in Vue
Tags: vue, javascript, css, webdev
Published: 2019-12-04 18:10:26

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](https://codepen.io/tahazsh/pen/mvKjbq) 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](https://tahazsh.com/test-out-vue-component-ideas). So create `App.vue`, and put the following into the `<template>` section:

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

```js
<script>
import Modal from './Modal'

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

And then this into `<style>`:

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

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

```js
<script>
export default {}
</script>
```

Next, add this for the styling:

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

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

```css
.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:

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

```js
<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.