The built-in <Transition>
component in Vue helps us to do animations when elements are added or removed with v-if
, v-show
or with dynamic components.
There is nothing wrong with using plain CSS transitions and animations in other cases.
This part of the tutorial requires knowledge about basic CSS animations and transitions.
But before we use the Vue specific built-in <Transition>
component to create animations, let's look at two examples of how plain CSS animations and transitions can be used with Vue.
App.vue
:
<template>
<h1>Basic CSS Transition</h1>
<button @click="this.doesRotate = true">Rotate</button>
<div :class="{ rotate: doesRotate }"></div>
</template>
<script>
export default {
data() {
return {
doesRotate: false
}
}
}
</script>
<style scoped>
.rotate {
rotate: 160deg;
transition: rotate 1s;
}
div {
border: solid black 2px;
background-color: lightcoral;
width: 60px;
height: 60px;
}
h1, button, div {
margin: 10px;
}
</style>
Run Example »
In the example above, we use v-bind
to give the <div>
tag a class so that it rotates. The reason that the rotation takes 1 second, is that it is defined with the CSS transition
property.
In the example below, we see how we can move an object with the CSS animation
property.
App.vue
:
<template>
<h1>Basic CSS Animation</h1>
<button @click="this.doesMove = true">Start</button>
<div :class="{ move: doesMove }"></div>
</template>
<script>
export default {
data() {
return {
doesMove: false
}
}
}
</script>
<style scoped>
.move {
animation: move .5s alternate 4 ease-in-out;
}
@keyframes move {
from {
translate: 0 0;
}
to {
translate: 70px 0;
}
}
div {
border: solid black 2px;
background-color: lightcoral;
border-radius: 50%;
width: 60px;
height: 60px;
}
h1, button, div {
margin: 10px;
}
</style>
Run Example »
There is nothing wrong with using plain CSS transitions and animations like we did in the two examples above.
But luckily Vue provides us with the built-in <Transition>
component in cases where we want to animate an element as it is removed from, or added to, our application with v-if
or v-show
, because that would be hard to do with plain CSS animation.
Let's first make an application where a button adds or removes a <p>
tag:
App.vue
:
<template>
<h1>Add/Remove <p> Tag</h1>
<button @click="this.exists = !this.exists">{{btnText}}</button><br>
<p v-if="exists">Hello World!</p>
</template>
<script>
export default {
data() {
return {
exists: false
}
},
computed: {
btnText() {
if(this.exists) {
return 'Remove';
}
else {
return 'Add';
}
}
}
}
</script>
<style>
p {
background-color: lightgreen;
display: inline-block;
padding: 10px;
}
</style>
Run Example »
Now let's wrap the <Transition>
component around the <p>
tag, and see how we can animate the removal of the <p>
tag.
When we use the <Transition>
component, we automatically get six different CSS classes we can use to animate when elements are added or removed.
In the example below we will use the automatically available classes v-leave-from
and v-leave-to
to make a fade out animation when the <p>
tag is removed:
App.vue
:
<template>
<h1>Add/Remove <p> Tag</h1>
<button @click="this.exists = !this.exists">{{btnText}}</button><br>
<Transition>
<p v-if="exists">Hello World!</p>
</Transition>
</template>
<script>
export default {
data() {
return {
exists: false
}
},
computed: {
btnText() {
if(this.exists) {
return 'Remove';
}
else {
return 'Add';
}
}
}
}
</script>
<style>
.v-leave-from {
opacity: 1;
}
.v-leave-to {
opacity: 0;
}
p {
background-color: lightgreen;
display: inline-block;
padding: 10px;
transition: opacity 0.5s;
}
</style>
Run Example »
There are six classes automatically available to us when we use the <Transition>
component.
As an element inside the <Transition>
component is added, we can use these first three classes to animate that transition:
And as an element is removed inside the <Transition>
component, we can use the next three classes :
Note: There can only be one element on root level of the <Transition>
component.
Now, let's use four of these classes so that we can animate both when the <p>
tag is added, and when it is removed.
App.vue
:
<template>
<h1>Add/Remove <p> Tag</h1>
<button @click="this.exists = !this.exists">{{btnText}}</button><br>
<Transition>
<p v-if="exists">Hello World!</p>
</Transition>
</template>
<script>
export default {
data() {
return {
exists: false
}
},
computed: {
btnText() {
if(this.exists) {
return 'Remove';
}
else {
return 'Add';
}
}
}
}
</script>
<style>
.v-enter-from {
opacity: 0;
translate: -100px 0;
}
.v-enter-to {
opacity: 1;
translate: 0 0;
}
.v-leave-from {
opacity: 1;
translate: 0 0;
}
.v-leave-to {
opacity: 0;
translate: 100px 0;
}
p {
background-color: lightgreen;
display: inline-block;
padding: 10px;
transition: all 0.5s;
}
</style>
Run Example »
We can also use the v-enter-active
and v-leave-active
to set styling or animation during adding or during removal of an element:
App.vue
:
<template>
<h1>Add/Remove <p> Tag</h1>
<button @click="this.exists = !this.exists">{{btnText}}</button><br>
<Transition>
<p v-if="exists">Hello World!</p>
</Transition>
</template>
<script>
export default {
data() {
return {
exists: false
}
},
computed: {
btnText() {
if(this.exists) {
return 'Remove';
}
else {
return 'Add';
}
}
}
}
</script>
<style>
.v-enter-active {
background-color: lightgreen;
animation: added 1s;
}
.v-leave-active {
background-color: lightcoral;
animation: added 1s reverse;
}
@keyframes added {
from {
opacity: 0;
translate: -100px 0;
}
to {
opacity: 1;
translate: 0 0;
}
}
p {
display: inline-block;
padding: 10px;
border: dashed black 1px;
}
</style>
Run Example »
In case you have several <Transition>
components, but you want at least one of the <Transition>
components to have a different animation, you need different names for the <Transition>
components to tell them apart.
We can choose the name of a <Transition>
component with the name
prop, and that changes the name of the transition classes as well, so that we can set different CSS animation rules for that component.
<Transition name="swirl">
If the transition name
prop value is set to 'swirl', the automatically available classes will now start with 'swirl-' instead of 'v-':
In the example below we use the name
prop to give the <Transition>
components different animations. One <Transition>
component is not given a name, and is therefore given animations using the automatically generated CSS classes starting with 'v-'. The other <Transition>
component is given a name 'swirl' so that it can be given rules for animation with the automatically generated CSS classes starting with 'swirl-'.
App.vue
:
<template>
<h1>Add/Remove <p> Tag</h1>
<p>The second transition in this example has the name prop "swirl", so that we can keep the transitions apart with different class names.</p>
<hr>
<button @click="this.p1Exists = !this.p1Exists">{{btn1Text}}</button><br>
<Transition>
<p v-if="p1Exists" id="p1">Hello World!</p>
</Transition>
<hr>
<button @click="this.p2Exists = !this.p2Exists">{{btn2Text}}</button><br>
<Transition name="swirl">
<p v-if="p2Exists" id="p2">Hello World!</p>
</Transition>
</template>
<script>
export default {
data() {
return {
p1Exists: false,
p2Exists: false
}
},
computed: {
btn1Text() {
if(this.p1Exists) {
return 'Remove';
}
else {
return 'Add';
}
},
btn2Text() {
if(this.p2Exists) {
return 'Remove';
}
else {
return 'Add';
}
}
}
}
</script>
<style>
.v-enter-active {
background-color: lightgreen;
animation: added 1s;
}
.v-leave-active {
background-color: lightcoral;
animation: added 1s reverse;
}
@keyframes added {
from {
opacity: 0;
translate: -100px 0;
}
to {
opacity: 1;
translate: 0 0;
}
}
.swirl-enter-active {
animation: swirlAdded 1s;
}
.swirl-leave-active {
animation: swirlAdded 1s reverse;
}
@keyframes swirlAdded {
from {
opacity: 0;
rotate: 0;
scale: 0.1;
}
to {
opacity: 1;
rotate: 360deg;
scale: 1;
}
}
#p1, #p2 {
display: inline-block;
padding: 10px;
border: dashed black 1px;
}
#p2 {
background-color: lightcoral;
}
</style>
Run Example »
Every Transition class as just mentioned corresponds to an event that we can hook into to run some JavaScript code.
Transition Class | JavaScript Event |
---|---|
v-enter-from | before-enter |
v-enter-active | enter |
v-enter-to | after-enter enter-cancelled |
v-leave-from | before-leave |
v-leave-active | leave |
v-leave-to | after-leave leave-cancelled (v-show only) |
When the after-enter
event happens in the example below, a method runs that displays a red <div>
element.
App.vue
:
<template>
<h1>JavaScript Transition Hooks</h1>
<p>This code hooks into "after-enter" so that after the initial animation is done, a method runs that displays a red div.</p>
<button @click="pVisible=true">Create p-tag!</button><br>
<Transition @after-enter="onAfterEnter">
<p v-show="pVisible" id="p1">Hello World!</p>
</Transition>
<br>
<div v-show="divVisible">This appears after the "enter-active" phase of the transition.</div>
</template>
<script>
export default {
data() {
return {
pVisible: false,
divVisible: false
}
},
methods: {
onAfterEnter() {
this.divVisible = true;
}
}
}
</script>
<style scoped>
.v-enter-active {
animation: swirlAdded 1s;
}
@keyframes swirlAdded {
from {
opacity: 0;
rotate: 0;
scale: 0.1;
}
to {
opacity: 1;
rotate: 360deg;
scale: 1;
}
}
#p1, div {
display: inline-block;
padding: 10px;
border: dashed black 1px;
}
#p1 {
background-color: lightgreen;
}
div {
background-color: lightcoral;
}
</style>
Run Example »
You can use the "Toggle" button in the example below to interrupt the enter transition phase of the <p>
element so that the enter-cancelled
event is triggered:
App.vue
:
<template>
<h1>The 'enter-cancelled' Event</h1>
<p>Click the toggle button again before the enter animation is finished to trigger the 'enter-cancelled' event.</p>
<button @click="pVisible=!pVisible">Toggle</button><br>
<Transition @enter-cancelled="onEnterCancelled">
<p v-if="pVisible" id="p1">Hello World!</p>
</Transition>
<br>
<div v-if="divVisible">You interrupted the "enter-active" transition.</div>
</template>
<script>
export default {
data() {
return {
pVisible: false,
divVisible: false
}
},
methods: {
onEnterCancelled() {
this.divVisible = true;
}
}
}
</script>
<style scoped>
.v-enter-active {
animation: swirlAdded 2s;
}
@keyframes swirlAdded {
from {
opacity: 0;
rotate: 0;
scale: 0.1;
}
to {
opacity: 1;
rotate: 720deg;
scale: 1;
}
}
#p1, div {
display: inline-block;
padding: 10px;
border: dashed black 1px;
}
#p1 {
background-color: lightgreen;
}
div {
background-color: lightcoral;
}
</style>
Run Example »
If we have an element that we want to animate when the page loads, we need to use the appear
prop on the <Transition>
component.
<Transition appear>
...
</Transition>
In this example, the appear
prop starts an animation when the page load for the first time:
App.vue
:
<template>
<h1>The 'appear' Prop</h1>
<p>The 'appear' prop starts the animation when the p tag below is rendered for the first time as the page opens. Without the 'appear' prop, this example would have had no animation.</p>
<Transition appear>
<p id="p1">Hello World!</p>
</Transition>
</template>
<style>
.v-enter-active {
animation: swirlAdded 1s;
}
@keyframes swirlAdded {
from {
opacity: 0;
rotate: 0;
scale: 0.1;
}
to {
opacity: 1;
rotate: 360deg;
scale: 1;
}
}
#p1 {
display: inline-block;
padding: 10px;
border: dashed black 1px;
background-color: lightgreen;
}
</style>
Run Example »
The <Transition>
component can also be used to switch between several elements, as long as we make sure that only one element is shown at a time with the use of <v-if>
and <v-else-if>
:
App.vue
:
<template>
<h1>Transition Between Elements</h1>
<p>Click the button to get a new image.</p>
<p>The new image is added before the previous is removed. We will fix this in the next example with mode="out-in".</p>
<button @click="newImg">Next image</button><br>
<Transition>
<img src="/img_pizza.svg" v-if="imgActive === 'pizza'">
<img src="/img_apple.svg" v-else-if="imgActive === 'apple'">
<img src="/img_cake.svg" v-else-if="imgActive === 'cake'">
<img src="/img_fish.svg" v-else-if="imgActive === 'fish'">
<img src="/img_rice.svg" v-else-if="imgActive === 'rice'">
</Transition>
</template>
<script>
export default {
data() {
return {
imgActive: 'pizza',
imgs: ['pizza', 'apple', 'cake', 'fish', 'rice'],
indexNbr: 0
}
},
methods: {
newImg() {
this.indexNbr++;
if(this.indexNbr >= this.imgs.length) {
this.indexNbr = 0;
}
this.imgActive = this.imgs[this.indexNbr];
}
}
}
</script>
<style>
.v-enter-active {
animation: swirlAdded 1s;
}
.v-leave-active {
animation: swirlAdded 1s reverse;
}
@keyframes swirlAdded {
from {
opacity: 0;
rotate: 0;
scale: 0.1;
}
to {
opacity: 1;
rotate: 360deg;
scale: 1;
}
}
img {
width: 100px;
margin: 20px;
}
img:hover {
cursor: pointer;
}
</style>
Run Example »
In the example above, the next image is added before the previous image is removed.
We use the mode="out-in"
prop and prop value on the <Transition>
component so that the removal of an element is finished before the next element is added.
In addition to mode="out-in"
, this example also uses a computed value 'imgActive' instead of the 'newImg' method we used in the previous example.
App.vue
:
<template>
<h1>mode="out-in"</h1>
<p>Click the button to get a new image.</p>
<p>With mode="out-in", the next image is not added until the current image is removed. Another difference from the previous example, is that here we use computed prop instead of a method.</p>
<button @click="indexNbr++">Next image</button><br>
<Transition mode="out-in">
<img src="/img_pizza.svg" v-if="imgActive === 'pizza'">
<img src="/img_apple.svg" v-else-if="imgActive === 'apple'">
<img src="/img_cake.svg" v-else-if="imgActive === 'cake'">
<img src="/img_fish.svg" v-else-if="imgActive === 'fish'">
<img src="/img_rice.svg" v-else-if="imgActive === 'rice'">
</Transition>
</template>
<script>
export default {
data() {
return {
imgs: ['pizza', 'apple', 'cake', 'fish', 'rice'],
indexNbr: 0
}
},
computed: {
imgActive() {
if(this.indexNbr >= this.imgs.length) {
this.indexNbr = 0;
}
return this.imgs[this.indexNbr];
}
}
}
</script>
<style>
.v-enter-active {
animation: swirlAdded 0.7s;
}
.v-leave-active {
animation: swirlAdded 0.7s reverse;
}
@keyframes swirlAdded {
from {
opacity: 0;
rotate: 0;
scale: 0.1;
}
to {
opacity: 1;
rotate: 360deg;
scale: 1;
}
}
img {
width: 100px;
margin: 20px;
}
img:hover {
cursor: pointer;
}
</style>
Run Example »
We can also use the <Transition>
component to animate switching between dynamic components:
App.vue
:
<template>
<h1>Transition with Dynamic Components</h1>
<p>The Transition component wraps around the dynamic component so that the switching can be animated.</p>
<button @click="toggleValue = !toggleValue">Switch component</button>
<Transition mode="out-in">
<component :is="activeComp"></component>
</Transition>
</template>
<script>
export default {
data () {
return {
toggleValue: true
}
},
computed: {
activeComp() {
if(this.toggleValue) {
return 'comp-one'
}
else {
return 'comp-two'
}
}
}
}
</script>
<style>
.v-enter-active {
animation: slideIn 0.5s;
}
@keyframes slideIn {
from {
translate: -200px 0;
opacity: 0;
}
to {
translate: 0 0;
opacity: 1;
}
}
.v-leave-active {
animation: slideOut 0.5s;
}
@keyframes slideOut {
from {
translate: 0 0;
opacity: 1;
}
to {
translate: 200px 0;
opacity: 0;
}
}
#app {
width: 350px;
margin: 10px;
}
#app > div {
border: solid black 2px;
padding: 10px;
margin-top: 10px;
}
</style>
Run Example »