Creating Reusable Transitions in Vue
Cristi Jora | February 26th, 2018 | 5 min read
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.
Click to load comments...