Cours JavaScript et React · Module 7: Styles et animations

Animations CSS et keyframes dans React

7 min de lecture
Dans cette leçon6

Les vaisseaux n'apparaissent pas brusquement sur l'écran radar : ils arrivent en volant, émettent un signal pulsé et se déplacent en douceur le long de leur orbite. Les animations dans les interfaces web fonctionnent selon le même principe : elles ajoutent du dynamisme à l'application et aident l'utilisateur à comprendre ce qui se passe. Dans cette leçon, tu découvriras des techniques pour créer des animations CSS dans les applications React - des simples transitions aux séquences complexes de keyframes, puis nous examinerons la bibliothèque React Transition Group.

CSS @keyframes avec styled-components

La règle @keyframes permet de définir une séquence d'étapes d'animation. Dans styled-components, nous utilisons l'utilitaire keyframes, qui génère un nom d'animation unique et évite les conflits :

1import styled, { keyframes } from 'styled-components';
2
3// Définition de l'animation - pulsation du signal radar
4const radarPulse = keyframes`
5  0% {
6    transform: scale(1);
7    opacity: 1;
8  }
9  50% {
10    transform: scale(1.5);
11    opacity: 0.5;
12  }
13  100% {
14    transform: scale(2);
15    opacity: 0;
16  }
17`;
18
19const RadarDot = styled.div`
20  width: 20px;
21  height: 20px;
22  border-radius: 50%;
23  background: #00ff88;
24  animation: ${radarPulse} 2s ease-out infinite;
25`;

Éléments clés :

  • keyframes est importé depuis styled-components (et non depuis le CSS)
  • À l'intérieur, on définit des étapes en pourcentage (0%, 50%, 100%) ou des mots-clés (from, to)
  • On utilise l'interpolation ${radarPulse} dans la propriété animation

Tu peux définir autant de keyframes que tu veux et les combiner dans différents composants :

1const floatInSpace = keyframes`
2  0%, 100% { transform: translateY(0px); }
3  50% { transform: translateY(-15px); }
4`;
5
6const rotateOrbit = keyframes`
7  from { transform: rotate(0deg); }
8  to { transform: rotate(360deg); }
9`;
10
11const SpaceStation = styled.div`
12  animation: ${floatInSpace} 4s ease-in-out infinite;
13`;
14
15const OrbitRing = styled.div`
16  animation: ${rotateOrbit} 10s linear infinite;
17`;

Transition - Changements d'état fluides

La propriété transition en CSS est la façon la plus simple d'animer les changements de valeur. Au lieu de définir des keyframes, tu dis au navigateur : « quand cette propriété change, fais-le en douceur ». C'est la solution idéale pour les effets de survol, les clics ou les changements d'état d'un composant.

1const NavButton = styled.button`
2  background: #1a1a3e;
3  color: #00ff88;
4  padding: 12px 24px;
5  border: 2px solid #00ff88;
6  border-radius: 8px;
7  cursor: pointer;
8
9  /* Définir ce qui doit être animé et comment */
10  transition: all 0.3s ease;
11
12  &:hover {
13    background: #00ff88;
14    color: #1a1a3e;
15    transform: scale(1.05);
16    box-shadow: 0 0 20px rgba(0, 255, 136, 0.4);
17  }
18`;

Syntaxe de transition :

1transition: property duration timing-function delay;
  • property - ce que l'on anime (all, background, transform, opacity...)
  • duration - combien de temps dure la transition (0.3s, 200ms)
  • timing-function - courbe de l'animation (ease, linear, ease-in-out, cubic-bezier())
  • delay - délai avant le démarrage (optionnel)

Tu peux animer plusieurs propriétés avec des paramètres différents :

1const ShipPanel = styled.div`
2  opacity: 0.8;
3  transform: translateX(0);
4  background: #0a0a2e;
5
6  transition:
7    opacity 0.2s ease,
8    transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
9    background 0.3s ease;
10
11  &:hover {
12    opacity: 1;
13    transform: translateX(10px);
14    background: #1a1a4e;
15  }
16`;

Animation - Animations continues

La propriété animation est une version plus avancée de transition. Elle permet des animations continues et répétées, une lecture inversée et le contrôle de chaque aspect du mouvement :

1const warningBlink = keyframes`
2  0%, 100% { opacity: 1; }
3  50% { opacity: 0.3; }
4`;
5
6const WarningLight = styled.div`
7  width: 16px;
8  height: 16px;
9  border-radius: 50%;
10  background: #ff4444;
11
12  animation-name: ${warningBlink};
13  animation-duration: 1s;
14  animation-timing-function: ease-in-out;
15  animation-iteration-count: infinite;
16  animation-direction: alternate;
17`;

Ou sous forme abrégée :

1animation: name duration timing-function delay iteration-count direction fill-mode;

Paramètres clés :

  • iteration-count - infinite (sans fin) ou nombre de répétitions (3)
  • direction - normal, reverse, alternate (aller-retour)
  • fill-mode - forwards (conserve l'état final), backwards, both
  • play-state - running ou paused (contrôle de la lecture)

Animations dynamiques basées sur les props

Dans styled-components, tu peux créer des animations qui réagissent aux props du composant. Cela permet d'adapter dynamiquement le mouvement à l'état de l'application :

1const moveShip = keyframes`
2  from { transform: translateX(0); }
3  to { transform: translateX(100%); }
4`;
5
6const Spaceship = styled.div`
7  width: 60px;
8  height: 30px;
9  background: #4488ff;
10  clip-path: polygon(0% 50%, 30% 0%, 100% 50%, 30% 100%);
11
12  animation: ${moveShip} ${props => props.speed || 3}s linear infinite;
13  animation-play-state: ${props => props.isFlying ? 'running' : 'paused'};
14  opacity: ${props => props.isFlying ? 1 : 0.5};
15  transition: opacity 0.3s ease;
16`;
17
18function FlightControl() {
19  const [isFlying, setIsFlying] = useState(false);
20  const [speed, setSpeed] = useState(3);
21
22  return (
23    <div>
24      <Spaceship isFlying={isFlying} speed={speed} />
25
26      <button onClick={() => setIsFlying(!isFlying)}>
27        {isFlying ? 'Arrêter' : 'Voler'}
28      </button>
29
30      <input
31        type="range"
32        min="0.5"
33        max="5"
34        step="0.5"
35        value={speed}
36        onChange={(e) => setSpeed(e.target.value)}
37      />
38      <span>Vitesse : {speed}s par orbite</span>
39    </div>
40  );
41}

Tu peux aussi sélectionner dynamiquement différentes keyframes selon l'état :

1const slideIn = keyframes`
2  from { transform: translateX(-100%); opacity: 0; }
3  to { transform: translateX(0); opacity: 1; }
4`;
5
6const slideOut = keyframes`
7  from { transform: translateX(0); opacity: 1; }
8  to { transform: translateX(100%); opacity: 0; }
9`;
10
11const Panel = styled.div`
12  animation: ${props => props.isVisible ? slideIn : slideOut} 0.4s ease forwards;
13`;

Cette approche permet d'animer l'entrée et la sortie d'un élément : le panneau glisse depuis la gauche en apparaissant et glisse vers la droite en disparaissant.

React Transition Group - CSSTransition

Le CSS natif ne peut pas animer les éléments ajoutés ou retirés du DOM (par exemple via le rendu conditionnel {show && <Component />}). La bibliothèque React Transition Group résout ce problème en gérant le cycle de vie de l'animation d'entrée/sortie.

Le composant le plus couramment utilisé est CSSTransition :

1import { CSSTransition } from 'react-transition-group';
2import { useState, useRef } from 'react';
3
4function NotificationPanel() {
5  const [showAlert, setShowAlert] = useState(false);
6  const nodeRef = useRef(null);
7
8  return (
9    <div>
10      <button onClick={() => setShowAlert(!showAlert)}>
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        nodeRef={nodeRef}
20      >
21        <div ref={nodeRef} className="alert-box">
22          Objet détecté sur le radar !
23        </div>
24      </CSSTransition>
25    </div>
26  );
27}

CSSTransition ajoute et supprime automatiquement les classes CSS aux moments appropriés :

1/* Entrée - début */
2.alert-enter {
3  opacity: 0;
4  transform: translateY(-20px);
5}
6
7/* Entrée - phase active */
8.alert-enter-active {
9  opacity: 1;
10  transform: translateY(0);
11  transition: opacity 300ms ease, transform 300ms ease;
12}
13
14/* Sortie - début */
15.alert-exit {
16  opacity: 1;
17  transform: translateY(0);
18}
19
20/* Sortie - phase active */
21.alert-exit-active {
22  opacity: 0;
23  transform: translateY(-20px);
24  transition: opacity 300ms ease, transform 300ms ease;
25}

Props clés de CSSTransition :

  • in - booléen qui contrôle la visibilité de l'élément
  • timeout - durée de l'animation (en ms), doit correspondre à la transition CSS
  • classNames - préfixe des classes CSS (par exemple "alert" génère alert-enter, alert-enter-active, etc.)
  • unmountOnExit - retire l'élément du DOM après l'animation de sortie
  • nodeRef - ref vers l'élément animé (recommandé pour éviter les avertissements)

Transition vs Animation - Laquelle utiliser ?

Récapitulons quand utiliser chaque technique :

transition - pour les changements d'état simples :

  • Effets de survol, focus, actif
  • Changements de couleur, taille, position lors d'une interaction
  • Entrées/sorties simples (opacity, transform)

@keyframes + animation - pour les animations complexes et continues :

  • Pulsation, rotation, flottement
  • Animations en plusieurs étapes (plus de 2 états)
  • Animations infinies d'arrière-plan ou de décoration

React Transition Group - pour les animations liées au cycle de vie des composants :

  • Montage/démontage animé des éléments
  • Listes avec ajout/suppression animés d'éléments
  • Transitions entre vues/pages

Des animations bien conçues sont comme le système de navigation d'un vaisseau : elles guident l'utilisateur, fournissent un retour d'information et rendent l'interface naturelle, comme traverser la galaxie avec une propulsion à distorsion plutôt que se téléporter sans prévenir.

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import styled, { keyframes, css, createGlobalStyle } from 'styled-components';
3
4// ===== GLOBAL STYLES =====
5const GlobalStyle = createGlobalStyle`
6  * { margin: 0; padding: 0; box-sizing: border-box; }
7  body {
8    font-family: 'Segoe UI', sans-serif;
9    background: #0a0a1a;
10    color: #e0e0e0;
11  }
12`;
13
14// ===== ANIMATIONS KEYFRAMES =====
15
16// 1. Rotation - idéale pour les spinners
17const spin = keyframes`
18  from { transform: rotate(0deg); }
19  to { transform: rotate(360deg); }
20`;
21
22// 2. Pulsation - effet de battement de cœur
23const pulse = keyframes`
24  0%, 100% { transform: scale(1); opacity: 1; }
25  50% { transform: scale(1.05); opacity: 0.8; }
26`;
27
28// 3. Effet de flottement (float)
29const float = keyframes`
30  0%, 100% { transform: translateY(0); }
31  50% { transform: translateY(-12px); }
32`;
33
34// 4. Effet de lueur (glow)
35const glow = keyframes`
36  0%, 100% { box-shadow: 0 0 5px rgba(0, 212, 255, 0.3); }
37  50% { box-shadow: 0 0 25px rgba(0, 212, 255, 0.6), 0 0 50px rgba(0, 212, 255, 0.3); }
38`;
39
40// 5. Entrée par la gauche
41const slideInLeft = keyframes`
42  from {
43    opacity: 0;
44    transform: translateX(-60px);
45  }
46  to {
47    opacity: 1;
48    transform: translateX(0);
49  }
50`;
51
52// TODO : Crée une animation fadeInUp
53// L'élément doit apparaître par le bas avec opacity 0
54// from: opacity 0, translateY(30px)
55// to: opacity 1, translateY(0)
56const fadeInUp = keyframes`
57  from {
58    opacity: 0;
59    transform: translateY(30px);
60  }
61  to {
62    opacity: 1;
63    transform: translateY(0);
64  }
65`;
66
67// ===== LAYOUT =====
68const Container = styled.div`
69  min-height: 100vh;
70  padding: 30px;
71  max-width: 700px;
72  margin: 0 auto;
73`;
74
75const Title = styled.h1`
76  text-align: center;
77  color: #00d4ff;
78  font-size: 2em;
79  margin-bottom: 5px;
80  animation: ${float} 3s ease-in-out infinite;
81`;
82
83const Subtitle = styled.p`
84  text-align: center;
85  color: #778da9;
86  margin-bottom: 35px;
87`;
88
89const Section = styled.div`
90  margin-bottom: 30px;
91  animation: ${slideInLeft} 0.6s ease-out;
92  animation-delay: ${p => p.delay || '0s'};
93  animation-fill-mode: both;
94`;
95
96const SectionTitle = styled.h3`
97  color: #6c5ce7;
98  margin-bottom: 15px;
99`;
100
101// ===== LOADING SPINNER =====
102const SpinnerWrapper = styled.div`
103  display: flex;
104  align-items: center;
105  gap: 15px;
106`;
107
108const Spinner = styled.div`
109  width: 40px;
110  height: 40px;
111  border: 3px solid rgba(0, 212, 255, 0.2);
112  border-top-color: #00d4ff;
113  border-radius: 50%;
114  animation: ${spin} 0.8s linear infinite;
115`;
116
117// ===== BOUTON PULSANT =====
118const PulseButton = styled.button`
119  padding: 12px 28px;
120  background: #6c5ce7;
121  color: white;
122  border: none;
123  border-radius: 10px;
124  font-size: 1rem;
125  font-weight: 600;
126  cursor: pointer;
127  animation: ${pulse} 2s ease-in-out infinite;
128  transition: background 0.3s;
129
130  &:hover {
131    background: #5a4bd1;
132    animation-play-state: paused;
133  }
134`;
135
136// ===== CARTE LUMINEUSE =====
137const GlowCard = styled.div`
138  background: rgba(255, 255, 255, 0.04);
139  border: 1px solid rgba(0, 212, 255, 0.2);
140  border-radius: 12px;
141  padding: 24px;
142  animation: ${glow} 3s ease-in-out infinite;
143  transition: transform 0.3s;
144
145  &:hover {
146    transform: scale(1.02);
147  }
148
149  h4 {
150    color: #00d4ff;
151    margin-bottom: 8px;
152  }
153`;
154
155// ===== CARTES ANIMÉES AVEC DÉLAI =====
156const StaggerCard = styled.div`
157  background: rgba(108, 92, 231, 0.1);
158  border-left: 4px solid #6c5ce7;
159  border-radius: 8px;
160  padding: 16px;
161  margin-bottom: 10px;
162  animation: ${fadeInUp} 0.5s ease-out both;
163  animation-delay: ${p => p.delay || '0s'};
164`;
165
166// TODO : Crée un composant AnimatedBadge avec une animation conditionnelle
167// Quand la prop active=true -> animation pulse
168// Quand active=false -> aucune animation
169const AnimatedBadge = styled.span`
170  display: inline-block;
171  padding: 4px 14px;
172  border-radius: 20px;
173  font-size: 0.8rem;
174  font-weight: 700;
175  background: ${p => p.active ? 'rgba(0, 184, 148, 0.2)' : 'rgba(99, 110, 114, 0.2)'};
176  color: ${p => p.active ? '#00b894' : '#636e72'};
177  animation: ${p => p.active ? css`${pulse} 2s infinite` : 'none'};
178`;
179
180// TODO : Crée un OrbitDot - un point qui orbite sur un cercle
181// Utilise @keyframes avec transform: rotate() et translateX()
182const orbit = keyframes`
183  from { transform: rotate(0deg) translateX(30px) rotate(0deg); }
184  to { transform: rotate(360deg) translateX(30px) rotate(-360deg); }
185`;
186
187const OrbitContainer = styled.div`
188  position: relative;
189  width: 80px;
190  height: 80px;
191  display: flex;
192  align-items: center;
193  justify-content: center;
194`;
195
196const OrbitCenter = styled.div`
197  width: 16px;
198  height: 16px;
199  background: #fdcb6e;
200  border-radius: 50%;
201  box-shadow: 0 0 15px rgba(253, 203, 110, 0.5);
202`;
203
204const OrbitDot = styled.div`
205  position: absolute;
206  width: 8px;
207  height: 8px;
208  background: #00d4ff;
209  border-radius: 50%;
210  animation: ${orbit} ${p => p.duration || '3s'} linear infinite;
211`;
212
213// ===== APPLICATION =====
214function App() {
215  const [showCards, setShowCards] = useState(false);
216
217  const missions = [
218    { name: 'Mission Alpha', status: true },
219    { name: 'Mission Beta', status: true },
220    { name: 'Mission Gamma', status: false },
221    { name: 'Mission Delta', status: true },
222  ];
223
224  return (
225    <>
226      <GlobalStyle />
227      <Container>
228        <Title>CSS Animations & Keyframes</Title>
229        <Subtitle>Animations en styled-components</Subtitle>
230
231        {/* Section : Spinner */}
232        <Section delay="0s">
233          <SectionTitle>Loading Spinner</SectionTitle>
234          <SpinnerWrapper>
235            <Spinner />
236            <span>Analyse du secteur cosmique...</span>
237          </SpinnerWrapper>
238        </Section>
239
240        {/* Section : Bouton pulsant */}
241        <Section delay="0.15s">
242          <SectionTitle>Bouton pulsant</SectionTitle>
243          <PulseButton onClick={() => setShowCards(!showCards)}>
244            {showCards ? 'Hide missions' : 'Show missions'}
245          </PulseButton>
246        </Section>
247
248        {/* Section : Cartes animées avec délai */}
249        {showCards && (
250          <Section delay="0s">
251            <SectionTitle>Staggered Cards</SectionTitle>
252            {missions.map((m, i) => (
253              <StaggerCard key={i} delay={i * 0.12 + 's'}>
254                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
255                  <span>{m.name}</span>
256                  <AnimatedBadge active={m.status}>
257                    {m.status ? 'ACTIVE' : 'OFFLINE'}
258                  </AnimatedBadge>
259                </div>
260              </StaggerCard>
261            ))}
262          </Section>
263        )}
264
265        {/* Section : Carte lumineuse */}
266        <Section delay="0.3s">
267          <SectionTitle>Glow Effect</SectionTitle>
268          <GlowCard>
269            <h4>Station spatiale Nebula-7</h4>
270            <p>Carte avec un effet de lueur pulsante CSS box-shadow.</p>
271          </GlowCard>
272        </Section>
273
274        {/* Section : Orbite */}
275        <Section delay="0.45s">
276          <SectionTitle>Orbit Animation</SectionTitle>
277          <div style={{ display: 'flex', alignItems: 'center', gap: 30 }}>
278            <OrbitContainer>
279              <OrbitCenter />
280              <OrbitDot duration="2.5s" />
281              <OrbitDot duration="4s" />
282            </OrbitContainer>
283            <span>Orbite avec rotate + translateX</span>
284          </div>
285        </Section>
286      </Container>
287    </>
288  );
289}
290
291export default App;

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

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Ordonne la définition d'une CSS Custom Property :

Articles utiles