Vue.js course ยท Module 13: Transitions & Animations
TransitionGroup - Animating Lists
In this lesson5
The NOVA LAB station module list is constantly changing - we add new life support systems, remove damaged sensors, sort alerts by priority. Each time, we want the list changes to be visually smooth. <TransitionGroup> is a Vue component designed specifically for animating elements rendered with v-for.
Basic Usage
1<TransitionGroup name="list" tag="ul">
2 <li v-for="item in items" :key="item.id">
3 {{ item.name }}
4 </li>
5</TransitionGroup>Each element added to the list animates on entry, and removed ones animate on exit. This automatic behavior is based on the same CSS classes as <Transition>.
Key Differences from <Transition>
<TransitionGroup> differs from <Transition> in several important ways:
It renders a DOM element - unlike
<Transition>,<TransitionGroup>renders an HTML element by default. You control it with thetagattribute:tag="ul",tag="div",tag="section". If you don't want an extra element, you can settag="template"(Vue 3.5+).Each element must have a unique
:key- Vue needs to track the identity of individual elements to know which ones to add, remove, and move. Never use the array index as a key!It supports multiple elements simultaneously - it's not limited to a single element like
<Transition>. Multiple elements can enter and leave at the same time.Additional
-moveclass - animates the movement of elements when the list order changes. This is a unique feature of<TransitionGroup>.The
modeattribute is not supported - because we're not switching between one element and another.
CSS Classes for Lists
TransitionGroup uses the same classes as Transition plus an additional -move class:
1/* New element entering */
2.list-enter-active {
3 transition: all 0.5s ease;
4}
5.list-enter-from {
6 opacity: 0;
7 transform: translateX(30px);
8}
9
10/* Element being removed */
11.list-leave-active {
12 transition: all 0.5s ease;
13 position: absolute; /* IMPORTANT! */
14}
15.list-leave-to {
16 opacity: 0;
17 transform: translateX(-30px);
18}
19
20/* Remaining elements moving */
21.list-move {
22 transition: transform 0.5s ease;
23}Move Transitions - The Key Mechanic
This is the most interesting feature of <TransitionGroup>. When you add or remove an element from a list, the remaining elements need to shift to new positions. Vue automatically adds the -move class to elements that change position, and animates their movement using CSS transforms.
For move transitions to work correctly, leaving elements must have position: absolute in the -leave-active class. This takes the leaving element "out of flow" so the rest can smoothly shift:
1.list-leave-active {
2 position: absolute;
3 /* Optionally: preserve width */
4 width: 100%;
5}
6
7.list-move {
8 transition: transform 0.5s ease;
9}Practical Example - Station Alert List
1<template>
2 <div class="alert-panel">
3 <button @click="addAlert">Add alert</button>
4
5 <TransitionGroup name="alert" tag="div" class="alert-list">
6 <div
7 v-for="alert in alerts"
8 :key="alert.id"
9 class="alert-item"
10 :class="alert.level"
11 >
12 {{ alert.message }}
13 <button @click="removeAlert(alert.id)">x</button>
14 </div>
15 </TransitionGroup>
16 </div>
17</template>
18
19<script setup>
20import { ref } from 'vue'
21
22const alerts = ref([])
23let nextId = 1
24
25function addAlert() {
26 alerts.value.unshift({
27 id: nextId++,
28 message: `Alert #${nextId - 1}: Oxygen level check`,
29 level: Math.random() > 0.5 ? 'warning' : 'critical'
30 })
31}
32
33function removeAlert(id) {
34 alerts.value = alerts.value.filter(a => a.id !== id)
35}
36</script>1.alert-enter-active, .alert-leave-active {
2 transition: all 0.4s ease;
3}
4.alert-enter-from {
5 opacity: 0;
6 transform: translateX(-30px);
7}
8.alert-leave-to {
9 opacity: 0;
10 transform: translateX(30px);
11}
12.alert-leave-active {
13 position: absolute;
14}
15.alert-move {
16 transition: transform 0.4s ease;
17}The effect: new alerts enter from the left, removed ones exit to the right, and the rest smoothly shift into place. Exactly like on a space station alert panel!
Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const modules = ref([
5 { id: 1, name: 'Oxygen Generator', status: 'active' },
6 { id: 2, name: 'Water Recycler', status: 'active' },
7 { id: 3, name: 'Solar Panel Array', status: 'active' }
8])
9
10let nextId = 4
11
12function addModule() {
13 const names = ['Thermal Shield', 'Nav Computer', 'Comm Relay', 'Greenhouse']
14 const name = names[Math.floor(Math.random() * names.length)]
15 modules.value.splice(
16 Math.floor(Math.random() * (modules.value.length + 1)),
17 0,
18 { id: nextId++, name, status: 'active' }
19 )
20}
21
22function removeModule(id) {
23 const idx = modules.value.findIndex(m => m.id === id)
24 if (idx > -1) modules.value.splice(idx, 1)
25}
26
27function shuffle() {
28 modules.value = [...modules.value].sort(() => Math.random() - 0.5)
29}
30</script>
31
32<template>
33 <div class="nova-lab">
34 <h1>TransitionGroup Demo</h1>
35
36 <div class="controls">
37 <button @click="addModule" class="btn add">+ Add module</button>
38 <button @click="shuffle" class="btn shuffle">Shuffle</button>
39 </div>
40
41 <TransitionGroup name="list" tag="ul" class="module-list">
42 <li v-for="mod in modules" :key="mod.id" class="module-item">
43 <span>{{ mod.name }}</span>
44 <button @click="removeModule(mod.id)" class="remove">ร</button>
45 </li>
46 </TransitionGroup>
47 </div>
48</template>
49
50<style scoped>
51.nova-lab {
52 background: #0a0e27;
53 color: #00ff88;
54 min-height: 100vh;
55 padding: 20px;
56 font-family: monospace;
57}
58h1 { color: #00ff88; }
59.controls { display: flex; gap: 10px; margin: 15px 0; }
60.btn {
61 border: none;
62 padding: 10px 16px;
63 border-radius: 8px;
64 cursor: pointer;
65 color: white;
66}
67.add { background: #00b4d8; }
68.shuffle { background: #9b59b6; }
69
70.module-list {
71 list-style: none;
72 padding: 0;
73}
74.module-item {
75 display: flex;
76 justify-content: space-between;
77 align-items: center;
78 background: rgba(0, 180, 216, 0.1);
79 border: 1px solid #00b4d8;
80 padding: 12px 16px;
81 margin: 8px 0;
82 border-radius: 8px;
83}
84.remove {
85 background: #e74c3c;
86 color: white;
87 border: none;
88 width: 28px;
89 height: 28px;
90 border-radius: 50%;
91 cursor: pointer;
92}
93
94/* TransitionGroup classes */
95.list-enter-active,
96.list-leave-active {
97 transition: all 0.4s ease;
98}
99.list-enter-from,
100.list-leave-to {
101 opacity: 0;
102 transform: translateX(30px);
103}
104/* Move animation */
105.list-move {
106 transition: transform 0.4s ease;
107}
108.list-leave-active {
109 position: absolute;
110}
111</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. How does <TransitionGroup> differ from <Transition>?
2. What does the tag='ul' attribute do in <TransitionGroup>?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Use <TransitionGroup> with v-for and name='list' to animate the list.
- Click in order
Arrange the correct TransitionGroup syntax:
- Code editor
Use the .list-move class with a transition on transform to animate movement.