Cours JavaScript et React · Module 7: Styles et animations
React Transition Group - Animer les voyages interstellaires de l'UI
Dans cette leçon6
Passer d'un état ou d'une vue à l'autre dans une application spatiale devrait être aussi fluide qu'un vaisseau filant à travers l'hyperespace. L'apparition et la disparition brusques d'éléments d'interface ressemblent plus à une panne système qu'à une expérience utilisateur bien conçue. Dans cette partie de la mission, nous allons découvrir React Transition Group, une bibliothèque qui nous aidera à créer des animations de transition fluides et cosmiquement réussies dans nos interfaces.
Qu'est-ce que React Transition Group ?
React Transition Group est une bibliothèque d'API bas niveau pour animer l'entrée et la sortie des composants. Contrairement aux solutions d'animation toutes faites, elle ne fournit pas d'effets prédéfinis : elle donne plutôt aux développeurs un contrôle total sur le processus d'animation en gérant les classes ou styles CSS aux moments clés du cycle de vie du composant.
C'est comme le panneau de contrôle d'un vaisseau : il ne pilote pas directement le vol, mais te donne les outils pour manœuvrer avec précision.
Les composants clés de React Transition Group
1. Transition
Le composant de base, de plus bas niveau, qui gère les états de transition d'un composant :
1import { Transition } from 'react-transition-group';
2import React, { useState } from 'react';
3
4// Styles pour les différents états
5const duration = 300;
6const defaultStyle = {
7 transition: `opacity ${duration}ms ease-in-out`,
8 opacity: 0,
9}
10
11const transitionStyles = {
12 entering: { opacity: 1 },
13 entered: { opacity: 1 },
14 exiting: { opacity: 0 },
15 exited: { opacity: 0 },
16};
17
18function FadeExample() {
19 const [inProp, setInProp] = useState(false);
20
21 return (
22 <div>
23 <button onClick={() => setInProp(!inProp)}>
24 {inProp ? 'Disparaître' : 'Apparaître'}
25 </button>
26
27 <Transition in={inProp} timeout={duration}>
28 {state => (
29 <div style={{
30 ...defaultStyle,
31 ...transitionStyles[state]
32 }}>
33 Message depuis l'espace
34 </div>
35 )}
36 </Transition>
37 </div>
38 );
39}2. CSSTransition
Étend Transition avec l'ajout et la suppression automatiques de classes CSS, ce qui facilite la création d'animations :
1import { CSSTransition } from 'react-transition-group';
2import React, { useState } from 'react';
3import './styles.css';
4
5function AlertExample() {
6 const [showAlert, setShowAlert] = useState(false);
7
8 return (
9 <div>
10 <button onClick={() => setShowAlert(prev => !prev)}>
11 {showAlert ? "Masquer l'alerte" : "Afficher l'alerte"}
12 </button>
13
14 <CSSTransition
15 in={showAlert}
16 timeout={300}
17 classNames="alert"
18 unmountOnExit
19 >
20 <div className="alert">
21 <h4>Message du quartier général !</h4>
22 <p>Une nouvelle planète propice à la colonisation a été détectée.</p>
23 </div>
24 </CSSTransition>
25 </div>
26 );
27}Et dans le fichier CSS :
1/* styles.css */
2.alert {
3 background-color: #121a2b;
4 color: white;
5 padding: 20px;
6 border-radius: 8px;
7 box-shadow: 0 0 20px rgba(82, 161, 255, 0.5);
8 margin-top: 20px;
9}
10
11.alert-enter {
12 opacity: 0;
13 transform: scale(0.9);
14}
15
16.alert-enter-active {
17 opacity: 1;
18 transform: scale(1);
19 transition: opacity 300ms, transform 300ms;
20}
21
22.alert-exit {
23 opacity: 1;
24 transform: scale(1);
25}
26
27.alert-exit-active {
28 opacity: 0;
29 transform: scale(0.9);
30 transition: opacity 300ms, transform 300ms;
31}3. TransitionGroup
Gère une collection de composants Transition ou CSSTransition, idéal pour animer des listes :
1import { TransitionGroup, CSSTransition } from 'react-transition-group';
2import React, { useState } from 'react';
3import './styles.css';
4
5function StarsList() {
6 const [items, setItems] = useState([
7 { id: 1, name: 'Alpha Centauri' },
8 { id: 2, name: 'Proxima Centauri' },
9 { id: 3, name: 'Betelgeuse' }
10 ]);
11
12 const [nextId, setNextId] = useState(4);
13
14 const addStar = () => {
15 const newStars = [
16 'Sirius', 'Vega', 'Polaris', 'Canopus', 'Arcturus',
17 'Antares', 'Rigel', 'Procyon', 'Deneb'
18 ];
19 const randomStar = newStars[Math.floor(Math.random() * newStars.length)];
20
21 setItems([...items, { id: nextId, name: randomStar }]);
22 setNextId(nextId + 1);
23 };
24
25 const removeStar = id => {
26 setItems(items.filter(item => item.id !== id));
27 };
28
29 return (
30 <div>
31 <h2>Étoiles à visiter</h2>
32 <button onClick={addStar}>Ajouter une étoile</button>
33
34 <TransitionGroup className="stars-list">
35 {items.map(({ id, name }) => (
36 <CSSTransition
37 key={id}
38 timeout={500}
39 classNames="star-item"
40 >
41 <div className="star-item">
42 <span>{name}</span>
43 <button onClick={() => removeStar(id)}>
44 ×
45 </button>
46 </div>
47 </CSSTransition>
48 ))}
49 </TransitionGroup>
50 </div>
51 );
52}Le CSS pour cet exemple :
1/* styles.css */
2.stars-list {
3 margin-top: 20px;
4}
5
6.star-item {
7 display: flex;
8 justify-content: space-between;
9 align-items: center;
10 padding: 12px 20px;
11 margin-bottom: 10px;
12 background-color: #0d1b2a;
13 border-radius: 8px;
14 color: #e0f7fa;
15 border-left: 4px solid #4fc3f7;
16}
17
18.star-item-enter {
19 opacity: 0;
20 transform: translateX(-30px);
21}
22
23.star-item-enter-active {
24 opacity: 1;
25 transform: translateX(0);
26 transition: opacity 500ms, transform 500ms;
27}
28
29.star-item-exit {
30 opacity: 1;
31}
32
33.star-item-exit-active {
34 opacity: 0;
35 transform: translateX(30px);
36 transition: opacity 500ms, transform 500ms;
37}Techniques avancées
1. Animations séquentielles
Animer les éléments l'un après l'autre pour un effet en cascade :
1import { CSSTransition, TransitionGroup } from 'react-transition-group';
2import React, { useState, useEffect } from 'react';
3import './styles.css';
4
5function LaunchSequence() {
6 const [steps, setSteps] = useState([]);
7 const allSteps = [
8 { id: 1, text: "Démarrage des systèmes" },
9 { id: 2, text: "Vérification du moteur principal" },
10 { id: 3, text: "Vérification des systèmes de navigation" },
11 { id: 4, text: "Verrouillage des sas" },
12 { id: 5, text: "Compte à rebours avant le lancement" }
13 ];
14
15 useEffect(() => {
16 if (steps.length < allSteps.length) {
17 const timer = setTimeout(() => {
18 setSteps(prevSteps => [
19 ...prevSteps,
20 allSteps[prevSteps.length]
21 ]);
22 }, 1000);
23
24 return () => clearTimeout(timer);
25 }
26 }, [steps]);
27
28 return (
29 <div className="launch-sequence">
30 <h2>Séquence de lancement</h2>
31 <TransitionGroup>
32 {steps.map(step => (
33 <CSSTransition
34 key={step.id}
35 timeout={500}
36 classNames="launch-step"
37 >
38 <div className="launch-step">
39 <div className="step-number">{step.id}</div>
40 <div className="step-text">{step.text}</div>
41 </div>
42 </CSSTransition>
43 ))}
44 </TransitionGroup>
45 </div>
46 );
47}2. Chorégraphie d'animations
Synchroniser plusieurs animations pour créer des séquences complexes :
1import { Transition } from 'react-transition-group';
2import React, { useState } from 'react';
3
4function SpaceshipDashboard() {
5 const [active, setActive] = useState(false);
6
7 // Durées pour les différents éléments
8 const durations = {
9 background: 500,
10 systems: 400,
11 controls: 600,
12 status: 700,
13 };
14
15 // Styles pour les différents états
16 const getStyles = (elementName, state) => {
17 const baseStyles = {
18 transition: `all ${durations[elementName]}ms ease-out`,
19 };
20
21 const elementSpecificStyles = {
22 background: {
23 entering: { opacity: 1, filter: 'brightness(1)' },
24 entered: { opacity: 1, filter: 'brightness(1)' },
25 exiting: { opacity: 0.5, filter: 'brightness(0.4)' },
26 exited: { opacity: 0.5, filter: 'brightness(0.4)' },
27 },
28 systems: {
29 entering: { opacity: 1, transform: 'translateY(0)' },
30 entered: { opacity: 1, transform: 'translateY(0)' },
31 exiting: { opacity: 0, transform: 'translateY(20px)' },
32 exited: { opacity: 0, transform: 'translateY(20px)' },
33 },
34 controls: {
35 entering: { opacity: 1, transform: 'scale(1)' },
36 entered: { opacity: 1, transform: 'scale(1)' },
37 exiting: { opacity: 0, transform: 'scale(0.8)' },
38 exited: { opacity: 0, transform: 'scale(0.8)' },
39 },
40 status: {
41 entering: { opacity: 1, transform: 'translateX(0)' },
42 entered: { opacity: 1, transform: 'translateX(0)' },
43 exiting: { opacity: 0, transform: 'translateX(-20px)' },
44 exited: { opacity: 0, transform: 'translateX(-20px)' },
45 },
46 };
47
48 return {
49 ...baseStyles,
50 ...elementSpecificStyles[elementName][state],
51 };
52 };
53
54 // Styles initiaux des éléments
55 const initialStyles = {
56 background: { opacity: 0.5, filter: 'brightness(0.4)' },
57 systems: { opacity: 0, transform: 'translateY(20px)' },
58 controls: { opacity: 0, transform: 'scale(0.8)' },
59 status: { opacity: 0, transform: 'translateX(-20px)' },
60 };
61
62 return (
63 <div className="dashboard-container">
64 <button onClick={() => setActive(!active)}>
65 {active ? 'Arrêter les systèmes' : 'Démarrer les systèmes'}
66 </button>
67
68 <div className="spaceship-dashboard">
69 <Transition in={active} timeout={durations.background}>
70 {state => (
71 <div
72 className="dashboard-background"
73 style={{
74 ...initialStyles.background,
75 ...getStyles('background', state)
76 }}
77 />
78 )}
79 </Transition>
80
81 <Transition in={active} timeout={durations.systems}>
82 {state => (
83 <div
84 className="system-panels"
85 style={{
86 ...initialStyles.systems,
87 ...getStyles('systems', state)
88 }}
89 >
90 Diagnostic des systèmes
91 </div>
92 )}
93 </Transition>
94
95 <Transition in={active} timeout={durations.controls}>
96 {state => (
97 <div
98 className="control-panels"
99 style={{
100 ...initialStyles.controls,
101 ...getStyles('controls', state)
102 }}
103 >
104 Commandes de vol
105 </div>
106 )}
107 </Transition>
108
109 <Transition in={active} timeout={durations.status}>
110 {state => (
111 <div
112 className="status-panels"
113 style={{
114 ...initialStyles.status,
115 ...getStyles('status', state)
116 }}
117 >
118 Moniteurs de statut
119 </div>
120 )}
121 </Transition>
122 </div>
123 </div>
124 );
125}Bonnes pratiques pour des animations cosmiquement fluides
Évite d'animer des propriétés coûteuses en calcul - des propriétés comme
width,heightettoppeuvent provoquer un reflow. Préfèretransformetopacity.Utilise des outils de débogage d'animations - des outils comme Chrome DevTools permettent d'analyser les performances des animations.
Réfléchis aux points de départ et d'arrivée - une animation bien conçue a des états clairement définis.
N'exagère pas la durée - les animations doivent être assez courtes pour ne pas retarder l'interaction de l'utilisateur (généralement 150-500ms).
Ajoute des délais pour les séquences complexes - utilise
delaypour créer des effets en cascade.Assure l'accessibilité - pense aux utilisateurs qui préfèrent peut-être des animations réduites (
prefers-reduced-motion).
Exemple : Portail interstellaire
Créons un composant portail qui anime la transition entre les « mondes » (vues) de notre application cosmique :
1import { CSSTransition } from 'react-transition-group';
2import React, { useState } from 'react';
3import './portal.css';
4
5function InterstellarPortal({ children, show, onExited }) {
6 return (
7 <CSSTransition
8 in={show}
9 timeout={1000}
10 classNames="portal"
11 unmountOnExit
12 onExited={onExited}
13 >
14 <div className="portal-container">
15 <div className="portal-content">
16 {children}
17 </div>
18 </div>
19 </CSSTransition>
20 );
21}
22
23function App() {
24 const [currentWorld, setCurrentWorld] = useState('home');
25 const [showPortal, setShowPortal] = useState(true);
26 const [nextWorld, setNextWorld] = useState(null);
27
28 const worlds = {
29 home: {
30 title: "Station spatiale Alpha",
31 content: "Bienvenue à bord de la station spatiale Alpha, le centre de commandement de notre opération interstellaire."
32 },
33 mars: {
34 title: "Colonie martienne One",
35 content: "Bienvenue dans la première colonie sur Mars. Les travaux de terraformation sont en cours."
36 },
37 europa: {
38 title: "Base sous-marine Europe",
39 content: "Bienvenue dans la base de recherche sous la coquille glacée d'Europe, la lune de Jupiter."
40 }
41 };
42
43 const navigateTo = (world) => {
44 setNextWorld(world);
45 setShowPortal(false);
46 };
47
48 const onPortalExited = () => {
49 setCurrentWorld(nextWorld);
50 setShowPortal(true);
51 };
52
53 return (
54 <div className="app">
55 <nav className="navigation">
56 <button onClick={() => navigateTo('home')} disabled={currentWorld === 'home'}>
57 Station Alpha
58 </button>
59 <button onClick={() => navigateTo('mars')} disabled={currentWorld === 'mars'}>
60 Mars
61 </button>
62 <button onClick={() => navigateTo('europa')} disabled={currentWorld === 'europa'}>
63 Europe
64 </button>
65 </nav>
66
67 <InterstellarPortal show={showPortal} onExited={onPortalExited}>
68 <h1>{worlds[currentWorld].title}</h1>
69 <p>{worlds[currentWorld].content}</p>
70 </InterstellarPortal>
71 </div>
72 );
73}Le CSS du portail :
1/* portal.css */
2.portal-container {
3 position: relative;
4 overflow: hidden;
5 min-height: 300px;
6 padding: 30px;
7 background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
8 border-radius: 12px;
9 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
10}
11
12.portal-content {
13 background: rgba(0, 0, 0, 0.7);
14 padding: 30px;
15 border-radius: 8px;
16 color: white;
17}
18
19/* Animation d'entrée */
20.portal-enter {
21 opacity: 0;
22 transform: scale(0.7);
23}
24
25.portal-enter-active {
26 opacity: 1;
27 transform: scale(1);
28 transition: opacity 1000ms ease-out, transform 1000ms cubic-bezier(0.34, 1.56, 0.64, 1);
29}
30
31/* Animation de sortie */
32.portal-exit {
33 opacity: 1;
34 transform: scale(1);
35}
36
37.portal-exit-active {
38 opacity: 0;
39 transform: scale(1.3);
40 transition: opacity 1000ms ease-in, transform 1000ms cubic-bezier(0.36, 0, 0.66, -0.56);
41}
42
43/* Style de la navigation */
44.navigation {
45 display: flex;
46 justify-content: center;
47 gap: 15px;
48 margin-bottom: 30px;
49}
50
51.navigation button {
52 background-color: #2c3e50;
53 color: white;
54 border: none;
55 padding: 10px 15px;
56 border-radius: 5px;
57 cursor: pointer;
58 transition: all 0.3s;
59}
60
61.navigation button:hover:not(:disabled) {
62 background-color: #34495e;
63 transform: translateY(-2px);
64}
65
66.navigation button:disabled {
67 background-color: #95a5a6;
68 cursor: not-allowed;
69}Exercice pratique : Crée une galerie de planètes animée
Conçois et implémente une galerie animée des planètes de notre système solaire. Lorsque l'utilisateur navigue entre les planètes, utilise React Transition Group pour créer des transitions fluides entre les vues. Pour chaque planète, affiche :
- Le nom de la planète
- Une image de la planète
- Quelques faits clés (taille, distance au Soleil, température)
Ajoute les animations suivantes :
- Une transition fluide entre les planètes
- Un effet en cascade pour afficher les faits sur la planète
- Un bouton « Retour à la vue d'ensemble » avec une animation de transition
N'oublie pas qu'une bonne animation enrichit l'expérience utilisateur, sans la perturber. Les animations doivent être fluides et naturelles, comme le voyage à travers l'espace lui-même : élégantes, majestueuses et inspirantes.
Code de cette leçon : App.jsx
1import React, { useState, useRef } from 'react';
2import './styles.css';
3
4// ===== SIMULATION REACT TRANSITION GROUP =====
5// Sandpack ne prend pas en charge react-transition-group,
6// on implémente donc des animations CSS avec des classes de transition
7
8// Composant CSSTransition - version simplifiée
9function CSSTransition({ inProp, timeout, children, classNames, unmountOnExit }) {
10 const [mounted, setMounted] = React.useState(inProp);
11 const [animClass, setAnimClass] = React.useState('');
12
13 React.useEffect(() => {
14 if (inProp) {
15 setMounted(true);
16 setAnimClass(classNames + '-enter');
17 requestAnimationFrame(() => {
18 requestAnimationFrame(() => {
19 setAnimClass(classNames + '-enter-active');
20 });
21 });
22 } else {
23 setAnimClass(classNames + '-exit');
24 requestAnimationFrame(() => {
25 requestAnimationFrame(() => {
26 setAnimClass(classNames + '-exit-active');
27 });
28 });
29 const timer = setTimeout(() => {
30 if (unmountOnExit) setMounted(false);
31 }, timeout);
32 return () => clearTimeout(timer);
33 }
34 }, [inProp]);
35
36 if (!mounted && unmountOnExit) return null;
37
38 return (
39 <div className={animClass}>
40 {children}
41 </div>
42 );
43}
44
45// ===== DEMO 1: Fade In/Out =====
46function FadeDemo() {
47 const [show, setShow] = useState(false);
48
49 return (
50 <div className="demo-section">
51 <h3>Fade In / Out</h3>
52 <button
53 className="demo-btn"
54 onClick={() => setShow(!show)}
55 >
56 {show ? 'Hide' : 'Show'} komunikat
57 </button>
58
59 <CSSTransition
60 inProp={show}
61 timeout={400}
62 classNames="fade"
63 unmountOnExit
64 >
65 <div className="alert-box">
66 Une nouvelle planète propice à la colonisation a été détectée !
67 </div>
68 </CSSTransition>
69 </div>
70 );
71}
72
73// ===== DEMO 2 : Liste avec animations =====
74function AnimatedList() {
75 const [items, setItems] = useState([
76 { id: 1, name: 'Alpha Centauri', visible: true },
77 { id: 2, name: 'Proxima Centauri', visible: true },
78 { id: 3, name: 'Betelgeuse', visible: true },
79 ]);
80 const nextId = useRef(4);
81
82 const starNames = [
83 'Sirius', 'Vega', 'Polaris', 'Canopus',
84 'Arcturus', 'Antares', 'Rigel', 'Procyon',
85 ];
86
87 const addStar = () => {
88 const name = starNames[Math.floor(Math.random() * starNames.length)];
89 const newItem = { id: nextId.current++, name, visible: false };
90 setItems(prev => [...prev, newItem]);
91 // Lance l'animation d'entrée après le montage
92 setTimeout(() => {
93 setItems(prev =>
94 prev.map(item =>
95 item.id === newItem.id ? { ...item, visible: true } : item
96 )
97 );
98 }, 50);
99 };
100
101 const removeStar = (id) => {
102 // Lance d'abord l'animation de sortie
103 setItems(prev =>
104 prev.map(item =>
105 item.id === id ? { ...item, visible: false } : item
106 )
107 );
108 // Puis supprime l'élément
109 setTimeout(() => {
110 setItems(prev => prev.filter(item => item.id !== id));
111 }, 500);
112 };
113
114 return (
115 <div className="demo-section">
116 <h3>Liste d'étoiles animée</h3>
117 <button className="demo-btn" onClick={addStar}>
118 + Ajouter une étoile
119 </button>
120
121 <div className="star-list">
122 {items.map(item => (
123 <div
124 key={item.id}
125 className={"star-item " + (item.visible ? 'star-item-enter-active' : 'star-item-exit-active')}
126 >
127 <span>{item.name}</span>
128 <button
129 className="remove-btn"
130 onClick={() => removeStar(item.id)}
131 >
132 ×
133 </button>
134 </div>
135 ))}
136 </div>
137 </div>
138 );
139}
140
141// TODO : Crée un composant SlideDemo avec une animation slide
142// Le composant doit :
143// 1. Avoir un bouton pour basculer la visibilité
144// 2. Animer l'élément avec un décalage depuis le haut (translateY)
145// 3. Utiliser les classes CSS : slide-enter, slide-enter-active, slide-exit-active
146function SlideDemo() {
147 const [show, setShow] = useState(false);
148
149 return (
150 <div className="demo-section">
151 <h3>Slide Transition</h3>
152 <button className="demo-btn" onClick={() => setShow(!show)}>
153 {show ? 'Réduire' : 'Déplier'} le panneau
154 </button>
155
156 {/* TODO : Ajoute un panneau animé avec la classe slide */}
157 {/* Utilise CSSTransition ou une logique de classes manuelle */}
158 <CSSTransition
159 inProp={show}
160 timeout={400}
161 classNames="slide"
162 unmountOnExit
163 >
164 <div className="slide-panel">
165 <h4>Panneau de contrôle de mission</h4>
166 <p>État des moteurs : ONLINE</p>
167 <p>Kurs: Alpha Centauri</p>
168 <p>ETA : 4.37 années-lumière</p>
169 </div>
170 </CSSTransition>
171 </div>
172 );
173}
174
175// ===== APPLICATION PRINCIPALE =====
176function App() {
177 return (
178 <div className="app-container">
179 <h1 className="app-title">React Transition Group</h1>
180 <p className="app-subtitle">Animations de transition dans une application cosmique</p>
181 <FadeDemo />
182 <AnimatedList />
183 <SlideDemo />
184 </div>
185 );
186}
187
188export default App;Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Tri vertical
Classe les étapes pour ajouter des animations CSS à un composant React :