Moving between different states and views in a space application should be as smooth as a spaceship flying through hyperspace. Sudden appearance and disappearance of interface elements feels more like a system failure than a well-designed user experience. In this part of the mission, we will learn about React Transition Group - a library that will help us create smooth, cosmically good transition animations in our interfaces.
React Transition Group is a low-level API library for component enter/exit animations. Unlike ready-made animation solutions, it does not provide predefined effects - instead, it gives developers full control over the animation process by managing CSS classes or styles at key moments in the component lifecycle.
It is like a spaceship control panel - it does not directly steer the flight, but gives you the tools for precise maneuvering.
TransitionThe basic lower-level component that manages component transition states:
1import { Transition } from 'react-transition-group';
2import React, { useState } from 'react';
3
4// Styles for different states
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 ? 'Disappear' : 'Appear'}
25 </button>
26
27 <Transition in={inProp} timeout={duration}>
28 {state => (
29 <div style={{
30 ...defaultStyle,
31 ...transitionStyles[state]
32 }}>
33 Message from space
34 </div>
35 )}
36 </Transition>
37 </div>
38 );
39}CSSTransitionExtends
Transition with automatic adding/removing of CSS classes, making it easier to create 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 ? 'Hide alert' : 'Show alert'}
12 </button>
13
14 <CSSTransition
15 in={showAlert}
16 timeout={300}
17 classNames="alert"
18 unmountOnExit
19 >
20 <div className="alert">
21 <h4>Message from headquarters!</h4>
22 <p>A new planet suitable for colonization has been detected.</p>
23 </div>
24 </CSSTransition>
25 </div>
26 );
27}And in the CSS file:
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}TransitionGroupManages a collection of
Transition or CSSTransition components, ideal for animating lists: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>Stars to visit</h2>
32 <button onClick={addStar}>Add star</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}CSS for this example:
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}Animating elements one after another for a cascading effect:
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: "Starting systems" },
9 { id: 2, text: "Main engine check" },
10 { id: 3, text: "Checking navigation systems" },
11 { id: 4, text: "Sealing hatches" },
12 { id: 5, text: "Countdown to launch" }
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>Launch Sequence</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}Synchronizing multiple animations to create complex sequences:
1import { Transition } from 'react-transition-group';
2import React, { useState } from 'react';
3
4function SpaceshipDashboard() {
5 const [active, setActive] = useState(false);
6
7 // Durations for different elements
8 const durations = {
9 background: 500,
10 systems: 400,
11 controls: 600,
12 status: 700,
13 };
14
15 // Styles for different states
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 // Initial styles for elements
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 ? 'Shut down systems' : 'Start systems'}
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 System Diagnostics
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 Flight Controls
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 Status Monitors
119 </div>
120 )}
121 </Transition>
122 </div>
123 </div>
124 );
125}Avoid animating computationally expensive properties - properties like
width, height, and top can cause reflow. Prefer transform and opacity.Use animation debuggers - tools like Chrome DevTools allow you to analyze animation performance.
Think through start and end points - a well-designed animation has clearly defined states.
Do not overdo the duration - animations should be short enough not to delay user interaction (usually 150-500ms).
Add delays for complex sequences - use
delay to create cascading effects.Ensure accessibility - remember users who may prefer reduced animations (
prefers-reduced-motion).Let's create a portal component that animates the transition between "worlds" (views) in our cosmic application:
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: "Space Station Alpha",
31 content: "Welcome aboard Space Station Alpha, the command center of our interstellar operation."
32 },
33 mars: {
34 title: "Mars Colony One",
35 content: "Welcome to the first colony on Mars. Terraforming work is in progress."
36 },
37 europa: {
38 title: "Underwater Base Europa",
39 content: "Welcome to the research base beneath the icy shell of Jupiter's moon - Europa."
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 Europa
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}CSS for the portal:
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/* Entry animation */
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/* Exit animation */
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/* Navigation style */
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}Design and implement an animated gallery of planets in our solar system. When a user navigates between planets, use React Transition Group to create smooth transitions between views. For each planet display:
Add the following animations:
Remember that good animation enhances the user experience, not distracts from it. Animations should be smooth and natural, like the journey through space itself - elegant, majestic, and inspiring.