Creating Reusable Transitions in Vue

Transitions in Vue.js are really great. There is no doubt that they can bring your app to life very easily but often you have to write them from scratch in each project or even bring some CSS library like animate.css to make them look good.

What if we could encapsulate these into components and simply reuse them across several projects? We will look at several ways to define transitions and dig our way to make them really reusable.

Raw transition component and CSS

The easiest way to define a transition is using the transition or transition-group components. This requires defining a name and some CSS for the transition.

App.vue

<template>
  <div id="app">
    <button v-on:click="show = !show">
      Toggle
    </button>
    <transition name="fade">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>
<script>
export default {
  name: "App",
  data() {
    return {
      show: true
    };
  }
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

Seems easy, right? However, there is a problem with this approach. We can’t really reuse this transition in another project.

Encapsulated transition component

What if we encapsulate the previous logic into a component and use it as a component instead?

FadeTransition.vue

<template>
  <transition name="fade">
    <slot></slot>
  </transition>
</template>
<script>
export default {
  
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

App.vue

<template>
  <div id="app">
    <button v-on:click="show = !show">
      Toggle transition
    </button>
    <fade-transition>
      <div v-if="show" class="box"></div>
    </fade-transition>
  </div>
</template>
<script>...</script>
<style>...</style>

By providing a slot in our transition component we could use it almost in the same way as a basic transition component. This is slightly better than the previous example but what if we want to pass other transition specific props such as mode or maybe even some hooks?

Encapsulated wrapper transition component

Fortunately, there is a feature in Vue that allows us to pass whatever extra props and listeners the user specifies to our internal tags/component. If you didn’t know yet, you can access extra passed props via $attrs and use them in combination with v-bind to bind them as props. The same applies to events via $listeners and apply them with v-on.

FadeTransition.vue

<template>
  <transition name="fade" v-bind="$attrs" v-on="$listeners">
    <slot></slot>
  </transition>
</template>
<script>
export default {};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

App.vue

...

<fade-transition mode="out-in">
  <div key="blue" v-if="show" class="box"></div>
  <div key="red" v-else class="red-box"></div>
</fade-transition>

...

Now we can pass any events and props that a normal transition component would accept which makes our component even more reusable. But why not take it a step further and add the possibility to customize the duration easily via a prop?

Explicit duration prop

Vue provides a duration prop for the transition component, however, it’s intended for more complex chained animations and it helps Vue chaining them together correctly.

What we really need in our case, is to control the CSS animation/transition via a component prop. We could achieve this by not specifying the explicit CSS animation duration in our CSS but rather apply it as a style. We can do that with the help of transition hooks which are pretty similar to component lifecycle hooks but they are called before and after transitioning the desired element. Let’s see how that looks in action.

FadeTransition.vue

<template>
  <transition name="fade"
              enter-active-class="fadeIn"
              leave-active-class="fadeOut"
              v-bind="$attrs"
              v-on="hooks">
      <slot></slot>
  </transition>
</template>
<script>
export default {
  props: {
    duration: {
      type: Number,
      default: 300
    }
  },
  computed: {
    hooks() {
      return {
        beforeEnter: this.setDuration,
        afterEnter: this.cleanUpDuration,
        beforeLeave: this.setDuration,
        afterLeave: this.cleanUpDuration,
        ...this.$listeners
      };
    }
  },
  methods: {
    setDuration(el) {
      el.style.animationDuration = `${this.duration}ms`;
    },
    cleanUpDuration(el) {
      el.style.animationDuration = "";
    }
  }
};
</script>
<style>
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.fadeIn {
  animation-name: fadeIn;
}
@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
.fadeOut {
  animation-name: fadeOut;
}
</style>

Now we have control over the real visible transition duration which makes our reusable transition flexible and easy to use. But what about transitioning multiple elements such as list items?

Transition group support

The most straightforward way that you think of would probably be to create a new component, let’s say fade-transition-group and replace the current transition tag with the transition-group one to achieve a group transition. What if we could do that in the same component and expose a group prop which will switch to a transition-group implementation? Fortunately, we can do that either with render functions or with the help of component and is attribute.

FadeTransition.vue

<template>
  <component :is="type"
             :tag="tag"
             enter-active-class="fadeIn"
             leave-active-class="fadeOut"
             move-class="fade-move"
             v-bind="$attrs"
             v-on="hooks">
      <slot></slot>
  </component>
</template>
<script>
export default {
  props: {
    duration: {
      type: Number,
      default: 300
    },
    group: {
      type: Boolean,
      default: false
    },
    tag: {
      type: String,
      default: "div"
    }
  },
  computed: {
    type() {
      return this.group ? "transition-group" : "transition";
    },
    hooks() {
      return {
        beforeEnter: this.setDuration,
        afterEnter: this.cleanUpDuration,
        beforeLeave: this.setDuration,
        afterLeave: this.cleanUpDuration,
        leave: this.setAbsolutePosition,
        ...this.$listeners
      };
    }
  },
  methods: {
    setDuration(el) {
      el.style.animationDuration = `${this.duration}ms`;
    },
    cleanUpDuration(el) {
      el.style.animationDuration = "";
    },
    setAbsolutePosition(el) {
      if (this.group) {
        el.style.position = "absolute";
      }
    }
  }
};
</script>
<style>
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.fadeIn {
  animation-name: fadeIn;
}
@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
.fadeOut {
  animation-name: fadeOut;
}
.fade-move {
  transition: transform 0.3s ease-out;
}
</style>

App.vue

...

<div class="box-wrapper">
  <fade-transition group :duration="300">
    <div class="box" 
         v-for="(item, index) in list" 
         @click="remove(index)"
         :key="item"
     >
    </div>
  </fade-transition>
</div>

...

There is one caveat with transition-group elements which is presented in the documentation. We basically have to set the position of each item to absolute when the element is leaving to achieve a smooth moving animation of the other items. We also have to add a move-class and manually specify the transition duration since there is no javascript hook for move Let’s add these tweaks to our previous example.

With a few more adjustments and by extracting our javascript logic in a mixin, we can apply it to easily create new transition components which we could simply drop in and use them in our next project.

Vue Transitions

Everything described until here is basically what this small Transition Collection contains. It has 10 encapsulated transition components at ~1kb (minified) each. I think it’s pretty handy and can be used with ease across different projects. Feel free to give it a try :)

Conclusion

We started from a basic transition example and managed to create reusable transition components in the end with adjustable duration and transition-group support. You can use these tips to create your own transition components based on your needs or who knows, maybe contribute to Vue Transitions and add more transitions there. Hopefully you learned something from this article and it will help you build beautiful transitions.

Cristi Jora's Picture

About Cristi Jora

Photography enthusiast 🖼 Vue.js ❤ Working together with @creativetimofficial to make |front end development fun and easy.

Comments