-
Notifications
You must be signed in to change notification settings - Fork 7
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.
Note: this article was originally posted here on the Vue.js Developers blog on 2017/08/27
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>
https://codesandbox.io/embed/6o1o1ljxn?module=%2Fsrc%2FApp.vue
Seems easy, right? However, there is a problem with this approach. We can’t really reuse this transition in another project.
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>
https://codesandbox.io/embed/1qk77050rl?module=%2Fsrc%2FFadeTransition.vue
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?
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>
...
https://codesandbox.io/embed/yjl1wjyoy1?module=%2Fsrc%2FFadeTransition.vue
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?
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>
https://codesandbox.io/embed/j4qnjvmwz9?module=%2Fsrc%2FFadeTransition.vue
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?
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>
...
https://codesandbox.io/embed/pk9r5j2257?module=%2Fsrc%2FFadeTransition.vue
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.
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 :)
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.
Get the latest Vue.js articles, tutorials and cool projects in your inbox with the Vue.js Developers Newsletter