Kurs JavaScript i React · Moduł 7: Stylowanie i animacje
Animacje CSS i keyframes w React
W tej lekcji6
Statki kosmiczne nie pojawiają się nagle na ekranie radaru - wlatują, pulsują sygnałem, płynnie przesuwają się po orbicie. Animacje w interfejsach webowych działają na tej samej zasadzie: nadają aplikacji dynamiki i pomagają użytkownikowi zrozumieć, co się dzieje. W tej lekcji poznasz techniki tworzenia animacji CSS w aplikacjach React - od prostych przejść po złożone sekwencje z keyframes, a na końcu zajrzymy do biblioteki React Transition Group.
CSS @keyframes z styled-components
Reguła @keyframes pozwala zdefiniować sekwencję kroków animacji. W styled-components korzystamy z helpera keyframes, który generuje unikalną nazwę animacji i zapobiega konfliktom:
1import styled, { keyframes } from 'styled-components';
2
3// Definicja animacji - pulsowanie sygnału radarowego
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`;Kluczowe elementy:
keyframesimportujemy zstyled-components(nie z CSS)- Wewnątrz definiujemy kroki procentowe (
0%,50%,100%) lub słowa kluczowe (from,to) - Używamy interpolacji
${radarPulse}w propercieanimation
Możesz definiować dowolnie wiele keyframes i łączyć je w różnych komponentach:
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 - płynne przejścia stanów
Właściwość transition w CSS to najprostszy sposób na animowanie zmian wartości. Zamiast definiować keyframes, mówisz przeglądarce: "gdy ta właściwość się zmieni, zrób to płynnie". To idealne rozwiązanie dla efektów hover, kliknięcia czy zmiany stanu komponentu.
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 /* Definiujemy, co ma być animowane i jak */
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`;Składnia transition:
1transition: property duration timing-function delay;property- co animujemy (all,background,transform,opacity...)duration- jak długo trwa przejście (0.3s,200ms)timing-function- krzywa animacji (ease,linear,ease-in-out,cubic-bezier())delay- opóźnienie startu (opcjonalne)
Możesz animować wiele właściwości z różnymi parametrami:
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 - ciągłe animacje
Właściwość animation to bardziej zaawansowana wersja transition. Pozwala na ciągłe, powtarzające się animacje, odtwarzanie wstecz i kontrolę nad każdym aspektem ruchu:
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`;Albo w skróconej formie:
1animation: name duration timing-function delay iteration-count direction fill-mode;Najważniejsze parametry:
iteration-count-infinite(nieskończone) lub liczba powtórzeń (3)direction-normal,reverse,alternate(tam i z powrotem)fill-mode-forwards(zachowaj stan końcowy),backwards,bothplay-state-runninglubpaused(kontrola odtwarzania)
Dynamiczne animacje oparte na props
W styled-components możesz tworzyć animacje, które reagują na props komponentu. To pozwala na dynamiczne dostosowanie ruchu do stanu aplikacji:
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 ? 'Zatrzymaj' : 'Leć'}
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>Prędkość: {speed}s na okrążenie</span>
39 </div>
40 );
41}Możesz też dynamicznie wybierać różne keyframes w zależności od stanu:
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`;To podejście pozwala na animowanie wejścia i wyjścia elementów - panel wsuwa się z lewej strony przy pojawieniu i wysuwa w prawo przy znikaniu.
React Transition Group - CSSTransition
Natywne CSS nie potrafi animować elementów, które są dodawane lub usuwane z DOM (np. przez warunkowe renderowanie {show && <Component />}). Biblioteka React Transition Group rozwiązuje ten problem, zarządzając cyklem życia animacji wejścia/wyjścia.
Najczęściej używany komponent to 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 ? 'Ukryj alert' : 'Pokaż alert'}
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 Wykryto obiekt na radarze!
23 </div>
24 </CSSTransition>
25 </div>
26 );
27}CSSTransition automatycznie dodaje i usuwa klasy CSS w odpowiednich momentach:
1/* Wejście - start */
2.alert-enter {
3 opacity: 0;
4 transform: translateY(-20px);
5}
6
7/* Wejście - aktywna faza */
8.alert-enter-active {
9 opacity: 1;
10 transform: translateY(0);
11 transition: opacity 300ms ease, transform 300ms ease;
12}
13
14/* Wyjście - start */
15.alert-exit {
16 opacity: 1;
17 transform: translateY(0);
18}
19
20/* Wyjście - aktywna faza */
21.alert-exit-active {
22 opacity: 0;
23 transform: translateY(-20px);
24 transition: opacity 300ms ease, transform 300ms ease;
25}Kluczowe propsy CSSTransition:
in- boolean kontrolujący, czy element jest widocznytimeout- czas trwania animacji (w ms), powinien odpowiadać CSS transitionclassNames- prefiks klas CSS (np."alert"generujealert-enter,alert-enter-activeitd.)unmountOnExit- usuwa element z DOM po animacji wyjścianodeRef- ref do animowanego elementu (zalecane dla uniknięcia ostrzeżeń)
Transition vs Animation - kiedy co wybrać?
Podsumujmy, kiedy używać poszczególnych technik:
transition - dla prostych zmian stanów:
- Efekty hover, focus, active
- Zmiana koloru, rozmiaru, pozycji przy interakcji
- Proste wejścia/wyjścia (opacity, transform)
@keyframes + animation - dla złożonych i ciągłych animacji:
- Pulsowanie, obracanie, latanie
- Animacje wieloetapowe (więcej niż 2 stany)
- Nieskończone animacje tła lub dekoracji
React Transition Group - dla animacji cyklu życia komponentów:
- Animowane montowanie/odmontowanie elementów
- Listy z animowanym dodawaniem/usuwaniem elementów
- Przejścia między widokami/stronami
Dobrze zaprojektowane animacje to jak system nawigacyjny statku - prowadzą użytkownika, dają feedback i sprawiają, że interfejs czuje się naturalnie, jak lot przez galaktykę z warp drive, a nie teleportacja bez ostrzeżenia.
Kod do tej lekcji: 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// ===== ANIMACJE KEYFRAMES =====
15
16// 1. Rotacja - idealna dla spinnerów
17const spin = keyframes`
18 from { transform: rotate(0deg); }
19 to { transform: rotate(360deg); }
20`;
21
22// 2. Pulsowanie - efekt bicia serca
23const pulse = keyframes`
24 0%, 100% { transform: scale(1); opacity: 1; }
25 50% { transform: scale(1.05); opacity: 0.8; }
26`;
27
28// 3. Efekt unoszenia się (float)
29const float = keyframes`
30 0%, 100% { transform: translateY(0); }
31 50% { transform: translateY(-12px); }
32`;
33
34// 4. Efekt świecenia (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. Wejście z lewej strony
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: Stwórz animację fadeInUp
53// Element powinien pojawiać się od dołu z 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// ===== PULSUJĄCY PRZYCISK =====
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// ===== ŚWIECĄCA KARTA =====
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// ===== ANIMOWANE KARTY Z OPÓŹNIENIEM =====
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: Stwórz komponent AnimatedBadge z animacją warunkową
167// Gdy prop active=true -> animacja pulse
168// Gdy active=false -> brak animacji
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: Stwórz OrbitDot - kropkę krążącą po okręgu
181// Użyj @keyframes z transform: rotate() i 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// ===== APLIKACJA =====
214function App() {
215 const [showCards, setShowCards] = useState(false);
216
217 const missions = [
218 { name: 'Missja Alpha', status: true },
219 { name: 'Missja Beta', status: true },
220 { name: 'Missja Gamma', status: false },
221 { name: 'Missja Delta', status: true },
222 ];
223
224 return (
225 <>
226 <GlobalStyle />
227 <Container>
228 <Title>CSS Animations & Keyframes</Title>
229 <Subtitle>Animacje w styled-components</Subtitle>
230
231 {/* Sekcja: Spinner */}
232 <Section delay="0s">
233 <SectionTitle>Loading Spinner</SectionTitle>
234 <SpinnerWrapper>
235 <Spinner />
236 <span>Skanowanie sektora kosmicznego...</span>
237 </SpinnerWrapper>
238 </Section>
239
240 {/* Sekcja: Pulsujący przycisk */}
241 <Section delay="0.15s">
242 <SectionTitle>Pulsujący przycisk</SectionTitle>
243 <PulseButton onClick={() => setShowCards(!showCards)}>
244 {showCards ? 'Ukryj misje' : 'Pokaż misje'}
245 </PulseButton>
246 </Section>
247
248 {/* Sekcja: Animowane karty z opóźnieniem */}
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 {/* Sekcja: Świecąca karta */}
266 <Section delay="0.3s">
267 <SectionTitle>Glow Effect</SectionTitle>
268 <GlowCard>
269 <h4>Stacja kosmiczna Nebula-7</h4>
270 <p>Karta z efektem pulsującego świecenia CSS box-shadow.</p>
271 </GlowCard>
272 </Section>
273
274 {/* Sekcja: Orbita */}
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>Orbita z użyciem rotate + translateX</span>
284 </div>
285 </Section>
286 </Container>
287 </>
288 );
289}
290
291export default App;Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Klikanie w kolejności
Ułóż definicję CSS Custom Property: