Vue Animations with v-for
The built-in <TransitionGroup>
component in Vue helps us to animate elements that are added to our page with v-for
.
The <TransitionGroup> Component
The <TransitionGroup>
component is used around elements created with v-for
, to give these elements individual animations when they are added or removed.
Tags created with v-for
inside the <TransitionGroup>
component must be defined with the key
attribute.
The <TransitionGroup>
component is only rendered as an HTML tag if we define it to be a specific tag by using the tag
prop, like this:
<TransitionGroup tag="ol">
<li v-for="x in products" :key="x">
{{ x }}
</li>
</TransitionGroup>
This is the result from the code above, after it is rendered by Vue:
<ol>
<li>Apple</li>
<li>Pizza</li>
<li>Rice</li>
</ol>
We can now add CSS code to animate new items when they are added to the list:
<style>
.v-enter-from {
opacity: 0;
rotate: 180deg;
}
.v-enter-to {
opacity: 1;
rotate: 0deg;
}
.v-enter-active {
transition: all 0.7s;
}
</style>
In this example, new items will be animated simply by adding them to 'products' array:
Example
App.vue
:
<template>
<h3>The <TransitionGroup> Component</h3>
<p>New products are given animations using the <TransitionGroup> component.</p>
<input type="text" v-model="inpName">
<button @click="addEl">Add</button>
<TransitionGroup tag="ol">
<li v-for="x in products" :key="x">
{{ x }}
</li>
</TransitionGroup>
</template>
<script>
export default {
data() {
return {
products: ['Apple','Pizza','Rice'],
inpName: ''
}
},
methods: {
addEl() {
const el = this.inpName;
this.products.push(el);
this.inpName = null;
}
}
}
</script>
<style>
.v-enter-from {
opacity: 0;
rotate: 180deg;
}
.v-enter-to {
opacity: 1;
rotate: 0deg;
}
.v-enter-active {
transition: all 0.7s;
}
</style>
Run Example »
Add and Remove Elements
When removing elements in between other elements, the other elements will fall into place where removed element was. To animate how the rest of the list items fall into place when an element is removed, we will use the automatically generated v-move
class.
But first, let's look at an example where it is not animated how other items fall into place when an element is removed:
Example
App.vue
:
<template>
<h3>The <TransitionGroup> Component</h3>
<p>New products are given animations using the <TransitionGroup> component.</p>
<button @click="addDie">Roll</button>
<button @click="removeDie">Remove random</button><br>
<TransitionGroup>
<div v-for="x in dice" :key="x" class="diceDiv" :style="{ backgroundColor: 'hsl('+x*40+',85%,85%)' }">
{{ x }}
</div>
</TransitionGroup>
</template>
<script>
export default {
data() {
return {
dice: [],
inpName: ''
}
},
methods: {
addDie() {
const newDie = Math.ceil(Math.random()*6);
this.dice.push(newDie);
},
removeDie() {
if(this.dice.length>0){
this.dice.splice(Math.floor(Math.random()*this.dice.length), 1);
}
}
},
mounted() {
this.addDie();
this.addDie();
this.addDie();
}
}
</script>
<style>
.v-enter-from {
opacity: 0;
translate: 200px 0;
rotate: 360deg;
}
.v-enter-to {
opacity: 1;
translate: 0 0;
rotate: 0deg;
}
.v-enter-active,
.v-leave-active {
transition: all 0.7s;
}
.v-leave-from { opacity: 1; }
.v-leave-to { opacity: 0; }
.diceDiv {
margin: 10px;
width: 30px;
height: 30px;
line-height: 30px;
vertical-align: middle;
text-align: center;
border: solid black 1px;
border-radius: 5px;
display: inline-block;
}
</style>
Run Example »
As you can see in the example above, when an item is removed, the items after the removed item suddenly jumps into their new positions. To animate the rest of the items when an item is removed, we use the automatically generated v-move
class.
The v-move
class animates the other elements as the removed item leaves, but there is one problem: The removed item still exists and takes up place until it is removed, and so the v-move
class will not have any effect. To give the v-move
class something to animate we can set position: absolute;
to the v-leave-active
class. When position: absolute;
is set during the removal period, the removed item is still visible, but does not take up place.
In this example, the only difference from the previous example are the two new CSS classes added on line 14 and 17:
Example
App.vue
:
<style>
.v-enter-from {
opacity: 0;
translate: 200px 0;
rotate: 360deg;
}
.v-enter-to {
opacity: 1;
translate: 0 0;
rotate: 0deg;
}
.v-enter-active,
.v-leave-active,
.v-move {
transition: all 0.7s;
}
.v-leave-active { position: absolute; }
.v-leave-from { opacity: 1; }
.v-leave-to { opacity: 0; }
.diceDiv {
margin: 10px;
width: 30px;
height: 30px;
line-height: 30px;
vertical-align: middle;
text-align: center;
border: solid black 1px;
border-radius: 5px;
display: inline-block;
}
</style>
Run Example »
A Larger Example
Let's use the example above as the basis for a new example.
In this example it becomes even more clear how the whole list is animated when a new item is added or removed, or when the whole array is sorted.
In this example we can:
- Remove items by clicking on them
- Sort the items
- Add new items at a random place in the list
Example
App.vue
:
<template>
<h3>The <TransitionGroup> Component</h3>
<p>Items inside the <TransitionGroup> component are animated when they are created or removed.</p>
<button @click="addDie">Roll</button>
<button @click="addDie10">Roll 10 dice</button>
<button @click="dice.sort(compareFunc)">Sort</button>
<button @click="dice.sort(shuffleFunc)">Shuffle</button><br>
<TransitionGroup>
<div
v-for="x in dice"
:key="x.keyNmbr"
class="diceDiv"
:style="{ backgroundColor: 'hsl('+x.dieNmbr*60+',85%,85%)' }"
@click="removeDie(x.keyNmbr)">
{{ x.dieNmbr }}
</div>
</TransitionGroup>
</template>
<script>
export default {
data() {
return {
dice: [],
keyNumber: 0
}
},
methods: {
addDie() {
const newDie = {
dieNmbr: Math.ceil(Math.random()*6),
keyNmbr: this.keyNumber
};
this.dice.splice(Math.floor(Math.random()*this.dice.length),0,newDie);
this.keyNumber++;
},
addDie10() {
for(let i=0; i<10; i++) {
this.addDie();
}
},
compareFunc(a,b){
return a.dieNmbr - b.dieNmbr;
},
shuffleFunc(a,b){
return Math.random()-0.5;
},
removeDie(key) {
const pos = this.dice.map(e => e.keyNmbr).indexOf(key);
this.dice.splice(pos, 1);
}
},
mounted() {
this.addDie10();
}
}
</script>
<style>
.v-enter-from {
opacity: 0;
scale: 0;
rotate: 360deg;
}
.v-enter-to {
opacity: 1;
scale: 1;
rotate: 0deg;
}
.v-enter-active,
.v-leave-active,
.v-move {
transition: all 0.7s;
}
.v-leave-active { position: absolute; }
.v-leave-from { opacity: 1; }
.v-leave-to { opacity: 0; }
.diceDiv {
margin: 10px;
width: 30px;
height: 30px;
line-height: 30px;
vertical-align: middle;
text-align: center;
border: solid black 1px;
border-radius: 5px;
display: inline-block;
}
.diceDiv:hover {
cursor: pointer;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
#app {
position: relative;
}
</style>
Run Example »