JavaScript and React course Β· Module 7: Styling and Animations
CSS Animations and Keyframes in React
In this lesson6
Spaceships do not suddenly appear on the radar screen - they fly in, pulse a signal, and smoothly move along their orbit. Animations in web interfaces work on the same principle: they add dynamics to the application and help the user understand what is happening. In this lesson, you will learn techniques for creating CSS animations in React applications - from simple transitions to complex keyframe sequences, and finally we will look at the React Transition Group library.
CSS @keyframes with styled-components
The @keyframes rule allows you to define a sequence of animation steps. In styled-components, we use the keyframes helper, which generates a unique animation name and prevents conflicts:
1import styled, { keyframes } from 'styled-components';
2
3// Animation definition - radar signal pulsing
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`;Key elements:
keyframesis imported fromstyled-components(not from CSS)- Inside we define percentage steps (
0%,50%,100%) or keywords (from,to) - We use interpolation
${radarPulse}in theanimationproperty
You can define any number of keyframes and combine them in different components:
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 - Smooth State Changes
The transition property in CSS is the simplest way to animate value changes. Instead of defining keyframes, you tell the browser: "when this property changes, do it smoothly." This is the ideal solution for hover effects, clicks, or component state changes.
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 /* Define what should be animated and how */
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`;transition syntax:
1transition: property duration timing-function delay;property- what we animate (all,background,transform,opacity...)duration- how long the transition lasts (0.3s,200ms)timing-function- animation curve (ease,linear,ease-in-out,cubic-bezier())delay- start delay (optional)
You can animate multiple properties with different parameters:
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 - Continuous Animations
The animation property is a more advanced version of transition. It allows for continuous, repeating animations, reverse playback, and control over every aspect of movement:
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`;Or in shorthand form:
1animation: name duration timing-function delay iteration-count direction fill-mode;Key parameters:
iteration-count-infinite(endless) or number of repetitions (3)direction-normal,reverse,alternate(back and forth)fill-mode-forwards(keep final state),backwards,bothplay-state-runningorpaused(playback control)
Dynamic Animations Based on Props
In styled-components, you can create animations that respond to component props. This allows dynamically adjusting movement to the application state:
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 ? 'Stop' : 'Fly'}
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>Speed: {speed}s per orbit</span>
39 </div>
40 );
41}You can also dynamically select different keyframes depending on state:
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`;This approach allows animating element entry and exit - the panel slides in from the left when appearing and slides out to the right when disappearing.
React Transition Group - CSSTransition
Native CSS cannot animate elements that are added to or removed from the DOM (e.g., through conditional rendering {show && <Component />}). The React Transition Group library solves this problem by managing the enter/exit animation lifecycle.
The most commonly used component is 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 ? 'Hide alert' : 'Show 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 Object detected on radar!
23 </div>
24 </CSSTransition>
25 </div>
26 );
27}CSSTransition automatically adds and removes CSS classes at the appropriate moments:
1/* Entry - start */
2.alert-enter {
3 opacity: 0;
4 transform: translateY(-20px);
5}
6
7/* Entry - active phase */
8.alert-enter-active {
9 opacity: 1;
10 transform: translateY(0);
11 transition: opacity 300ms ease, transform 300ms ease;
12}
13
14/* Exit - start */
15.alert-exit {
16 opacity: 1;
17 transform: translateY(0);
18}
19
20/* Exit - active phase */
21.alert-exit-active {
22 opacity: 0;
23 transform: translateY(-20px);
24 transition: opacity 300ms ease, transform 300ms ease;
25}Key CSSTransition props:
in- boolean controlling whether the element is visibletimeout- animation duration (in ms), should match the CSS transitionclassNames- CSS class prefix (e.g.,"alert"generatesalert-enter,alert-enter-active, etc.)unmountOnExit- removes the element from DOM after exit animationnodeRef- ref to the animated element (recommended to avoid warnings)
Transition vs Animation - When to Use Which?
Let's summarize when to use each technique:
transition - for simple state changes:
- Hover, focus, active effects
- Color, size, position changes on interaction
- Simple entries/exits (opacity, transform)
@keyframes + animation - for complex and continuous animations:
- Pulsing, rotating, floating
- Multi-step animations (more than 2 states)
- Infinite background or decoration animations
React Transition Group - for component lifecycle animations:
- Animated mounting/unmounting of elements
- Lists with animated adding/removing of elements
- Transitions between views/pages
Well-designed animations are like a ship's navigation system - they guide the user, provide feedback, and make the interface feel natural, like flying through the galaxy with a warp drive, not teleporting without warning.
Code for this lesson: 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// ===== KEYFRAME ANIMATIONS =====
15
16// 1. Rotation - ideal for spinners
17const spin = keyframes`
18 from { transform: rotate(0deg); }
19 to { transform: rotate(360deg); }
20`;
21
22// 2. Pulsing - heartbeat effect
23const pulse = keyframes`
24 0%, 100% { transform: scale(1); opacity: 1; }
25 50% { transform: scale(1.05); opacity: 0.8; }
26`;
27
28// 3. Floating effect (float)
29const float = keyframes`
30 0%, 100% { transform: translateY(0); }
31 50% { transform: translateY(-12px); }
32`;
33
34// 4. Glowing effect (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. Entrance from the left side
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: Create the fadeInUp animation
53// The element should appear from the bottom with 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// ===== PULSING BUTTON =====
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// ===== GLOWING CARD =====
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// ===== ANIMATED CARDS WITH DELAY =====
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: Create an AnimatedBadge component with conditional animation
167// When prop active=true -> pulse animation
168// When active=false -> no 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: Create OrbitDot - a dot orbiting in a circle
181// Use @keyframes with transform: rotate() and 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 in styled-components</Subtitle>
230
231 {/* Section: Spinner */}
232 <Section delay="0s">
233 <SectionTitle>Loading Spinner</SectionTitle>
234 <SpinnerWrapper>
235 <Spinner />
236 <span>Scanning space sector...</span>
237 </SpinnerWrapper>
238 </Section>
239
240 {/* Section: Pulsing button */}
241 <Section delay="0.15s">
242 <SectionTitle>Pulsing button</SectionTitle>
243 <PulseButton onClick={() => setShowCards(!showCards)}>
244 {showCards ? 'Hide missions' : 'Show missions'}
245 </PulseButton>
246 </Section>
247
248 {/* Section: Animated cards with delay */}
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: Glowing card */}
266 <Section delay="0.3s">
267 <SectionTitle>Glow Effect</SectionTitle>
268 <GlowCard>
269 <h4>Nebula-7 space station</h4>
270 <p>Card with a pulsing CSS box-shadow glow effect.</p>
271 </GlowCard>
272 </Section>
273
274 {/* Section: Orbit */}
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>Orbit using rotate + translateX</span>
284 </div>
285 </Section>
286 </Container>
287 </>
288 );
289}
290
291export default App;Spotted a mistake in this lesson?
Hands-on tasks in the game
- Click in order
Arrange the CSS Custom Property definition: