Cours Vue.js · Module 13: Transitions et animations

Projet : système d'animation du NOVA LAB

3 min de lecture
Dans cette leçon4

Il est temps de combiner toutes les techniques d'animation en un système d'interface cohérent pour la station spatiale ! Dans ce projet, tu vas créer un tableau de bord complet du NOVA LAB qui utilise <Transition>, <TransitionGroup>, les transitions nommées, le mode out-in, les animations échelonnées, appear et l'accessibilité - tout ce que tu as appris dans ce module.

Récapitulatif du module

Dans ce module, tu as appris :

  • <Transition> - animer l'entrée/sortie d'éléments uniques avec 6 classes CSS
  • Transitions nommées - définir plusieurs types d'animation avec l'attribut name
  • Modes de transition (mode) - contrôler l'ordre d'animation lors du basculement de vues (out-in, in-out)
  • Hooks d'animation JavaScript - contrôle total sur les animations via @enter, @leave et le callback done()
  • <TransitionGroup> - animer des listes avec v-for, la classe -move pour le déplacement des éléments
  • Intégration de bibliothèques - noms de classes personnalisés pour Animate.css, GSAP via les hooks JS
  • Transitions de route - transitions de page fluides avec <RouterView>
  • Appear - animation au premier rendu, chargement échelonné
  • Accessibilité - respecter prefers-reduced-motion

Exigences du projet

Construis un tableau de bord animé de la station NOVA LAB avec les éléments suivants :

1. Panneau de contrôle avec animation de fondu

Un panneau principal qu'on peut afficher/masquer avec un bouton. Utilise <Transition name="fade"> avec v-if.

2. Basculement de vue avec le mode out-in

À l'intérieur du panneau, implémente des onglets (par exemple « Statut » et « Alertes »). Le basculement entre eux doit utiliser <Transition name="slide" mode="out-in"> avec un :key dynamique.

3. Liste d'alertes avec TransitionGroup

Dans l'onglet « Alertes », affiche une liste d'alertes avec la possibilité d'ajouter et de retirer des éléments. Utilise <TransitionGroup name="list"> avec les classes enter, leave et move.

4. Chargement échelonné des éléments au montage

La liste des modules de la station doit apparaître en cascade au chargement de l'application. Utilise <TransitionGroup appear :css="false"> avec des hooks JS et un délai basé sur l'index.

5. Accessibilité

Ajoute la prise en charge de prefers-reduced-motion - les utilisateurs qui ne veulent pas d'animations doivent voir des changements instantanés sans transitions.

Architecture

1App.vue
2├── Bouton bascule (afficher/masquer le panneau)
3├── Panneau de contrôle (Transition name="fade")
4│   ├── Onglets (statut / alertes)
5│   │   └── Transition name="slide" mode="out-in"
6│   │       ├── Vue statut
7│   │       │   └── Affichage des métriques système
8│   │       └── Vue alertes
9│   │           ├── Bouton d'ajout d'alerte
10│   │           └── TransitionGroup name="list"
11│   │               └── Éléments d'alerte (ajout/suppression)
12│   └── Liste des modules système (TransitionGroup appear :css="false")
13│       └── Éléments échelonnés (délai basé sur l'index)

Modèles clés à appliquer

  1. Transitions imbriquées - fondu sur le panneau, glissement sur les onglets, liste sur les alertes
  2. Plusieurs techniques dans un projet - transitions CSS (fade, slide), move CSS (list), hooks JS (stagger)
  3. TransitionGroup avec move - ajouter une alerte en début de liste fait que les autres se décalent en douceur
  4. Appear avec stagger - le chargement en cascade des modules crée l'effet de « mise sous tension des systèmes de la station »
  5. Le callback done() - dans les hooks JS, pense à appeler done() après la fin de l'animation

Essaie d'enrichir le projet avec des effets supplémentaires - par exemple une animation bounce pour les alertes critiques ou un effet de pulsation pour les modules actifs !

Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3
4const showPanel = ref(false)
5const alerts = ref([
6  { id: 1, text: 'Système de survie : OK', type: 'success' },
7  { id: 2, text: 'Niveau d\'oxygène : 21%', type: 'info' }
8])
9const currentView = ref('status')
10let alertId = 3
11
12function addAlert() {
13  const texts = [
14    'Anomalie de température détectée',
15    'Efficacité des panneaux solaires : 87%',
16    'Signal du relais de comm puissant',
17    'Maintenance du recycleur d\'eau requise'
18  ]
19  alerts.value.push({
20    id: alertId++,
21    text: texts[Math.floor(Math.random() * texts.length)],
22    type: Math.random() > 0.5 ? 'info' : 'success'
23  })
24}
25
26function removeAlert(id) {
27  alerts.value = alerts.value.filter(a => a.id !== id)
28}
29</script>
30
31<template>
32  <div class="nova-lab">
33    <h1>NOVA LAB - Système d'animation (Projet)</h1>
34
35    <!-- Bascule du panneau avec transition fade -->
36    <button @click="showPanel = !showPanel" class="btn primary">
37      {{ showPanel ? 'Fermer' : 'Ouvrir' }} le panneau de contrôle
38    </button>
39
40    <Transition name="fade">
41      <div v-if="showPanel" class="control-panel">
42        <h2>Panneau de contrôle</h2>
43
44        <!-- Basculeur de vue avec slide + out-in -->
45        <div class="tabs">
46          <button @click="currentView = 'status'" :class="{active: currentView === 'status'}">Statut</button>
47          <button @click="currentView = 'alerts'" :class="{active: currentView === 'alerts'}">Alertes</button>
48        </div>
49
50        <Transition name="slide" mode="out-in">
51          <div v-if="currentView === 'status'" key="status" class="view">
52            <p>Tous les systèmes opérationnels</p>
53          </div>
54          <div v-else key="alerts" class="view">
55            <button @click="addAlert" class="btn small">+ Alerte</button>
56            <TransitionGroup name="list" tag="div">
57              <div v-for="alert in alerts" :key="alert.id" :class="['alert', alert.type]">
58                {{ alert.text }}
59                <button @click="removeAlert(alert.id)" class="close">×</button>
60              </div>
61            </TransitionGroup>
62          </div>
63        </Transition>
64      </div>
65    </Transition>
66  </div>
67</template>
68
69<style scoped>
70.nova-lab {
71  background: #0a0e27;
72  color: #00ff88;
73  min-height: 100vh;
74  padding: 20px;
75  font-family: monospace;
76}
77h1, h2 { color: #00ff88; }
78.btn { border: none; padding: 10px 16px; border-radius: 8px; cursor: pointer; color: white; }
79.primary { background: #00b4d8; margin-bottom: 15px; }
80.small { background: #9b59b6; font-size: 12px; padding: 6px 12px; margin-bottom: 10px; }
81
82.control-panel {
83  background: rgba(0, 180, 216, 0.05);
84  border: 2px solid #00b4d8;
85  padding: 20px;
86  border-radius: 12px;
87}
88.tabs { display: flex; gap: 8px; margin: 10px 0; }
89.tabs button {
90  background: rgba(0,180,216,0.1);
91  color: #00b4d8;
92  border: 1px solid #00b4d8;
93  padding: 6px 14px;
94  border-radius: 6px;
95  cursor: pointer;
96}
97.tabs button.active { background: #00b4d8; color: white; }
98
99.view { min-height: 100px; }
100.alert {
101  display: flex;
102  justify-content: space-between;
103  align-items: center;
104  padding: 10px 14px;
105  margin: 6px 0;
106  border-radius: 6px;
107}
108.alert.success { background: rgba(0,255,136,0.1); border: 1px solid #00ff88; }
109.alert.info { background: rgba(0,180,216,0.1); border: 1px solid #00b4d8; }
110.close { background: transparent; border: none; color: #e74c3c; cursor: pointer; font-size: 16px; }
111
112/* Fade */
113.fade-enter-active, .fade-leave-active { transition: opacity 0.4s ease; }
114.fade-enter-from, .fade-leave-to { opacity: 0; }
115
116/* Slide */
117.slide-enter-active, .slide-leave-active { transition: all 0.3s ease; }
118.slide-enter-from { opacity: 0; transform: translateX(20px); }
119.slide-leave-to { opacity: 0; transform: translateX(-20px); }
120
121/* List */
122.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
123.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
124.list-move { transition: transform 0.3s ease; }
125.list-leave-active { position: absolute; }
126</style>

Tu as repéré une erreur dans cette leçon ?