Say you have a list component in React where the user can add or remove items in the list. It would be nice to animate the items as they are being added or removed from the list.
In this tutorial, we'll cover how to animate components' entrance and exit in React using React Transition Group.
You can find the full code for this tutorial in this GitHub Repository, and you can see a working demo.
What is React Transition Group
React Transition Group is a library that allows you to add animation on a component or multiple components' entrance and exit.
React Transition Group does NOT do the animation for you, that is it does not provide the animation. It facilitates adding the animation either through CSS classes or styles when a component enters or exits.
React Transition Group exposes the components that will allow you to easily do that. There are 4 components that it exposes: Transition, CSSTransition, SwitchTransition, and TransitionGroup.
We'll go over different use cases when it comes to animating elements, and in each use case which component you should use and how you can use them.
Animating a Single Element
The first use case we'll look at is animating a single element. Let's say we have an element that we want to animate every time it enters or exits.
There are 2 components we can use: Transition
and CSSTransition
. The recommended component is CSSTransition
, but we'll cover both.
Using Transition
With the Transition
component, you can add CSS styling based on the different states. This component covers the states:
entering
: Before the element enters.entered
: Element has entered.exiting
: Before the element exitsexited
: The element has exited.
Generally, CSSTransition
is recommended to be used instead of Transition
. Transition
is provided as a platform-agnostic base component.
For this example, we'll have a button that will allow us to show or hide a picture of a cat. First, we need to create a state variable to store and indicate whether the image should be shown or not.
const [transitionState, setTransitionState] = useState(false)
Then, We'll use the Transition
component to wrap the img
element. The Transition
component takes the prop in
which is a boolean variable that indicates whether the component should enter or not. We should pass the state variable to this prop.
Another required prop that Transition
accepts is timeout
which defines the duration of the animation.
<Transition in={transitionState} timeout={300} >
...
</Transition
Inside Transition
, a function is passed which receives the state
parameter. This parameter indicates the current state of the component, which will be one of the 4 states mentioned earlier.
Using that state
variable we can change the CSS styling of the component to animate it.
So, we need to create an object that holds the stylings we want to apply:
const transitions = {
entering: {
display: 'block'
},
entered: {
opacity: 1,
display: 'block'
},
exiting: {
opacity: 0,
display: 'block'
},
exited: {
opacity: '0',
display: 'none'
}
};
Notice how we set the object keys or properties as the name of the states.
Then, in the child function of Transition
, we set the style based on the current state:
<Transition in={transitionState} timeout={300} >
{state => (
<img src="https://cataas.com/cat" alt="Cat" style={{
transition: 'all .1s',
opacity: 0,
display: 'none',
...transitions[state]
}} className="mt-2" />
)}
</Transition>
Notice how the function returns the img
element. Inside the style
prop of the img
element we first set the default styling, then we add the styling based on the state using this line:
...transitions[state]
Now, everytime the state changes when the component enters or exits, the state
variable in the child function will change. So, the styling of the element will change based on the value of the state
variable, which will add animation to the element.
Also, the image we're using is from Cat as a service.
The only thing left is to add a button to toggle the state variable transitionState
to show and hide the image:
<Button onClick={() => setTransitionState(!transitionState)}>{transitionState ? 'Hide' : 'Show'} Cat</Button>
Using CSSTransition
The recommended approach for this use case is using CSSTransition
. The CSSTransition
component allows you to add classes for each state, which gives you more freedom to add animation to your components.
To make the animation easier we'll use Animate.css which is a CSS animation library that provides us with many animations we can easily use.
To animate an element with CSSTransition
, you wrap it within the CSSTransition
component. Similar to Transition
CSSTransition
receives the in
prop which indicates whether the component should enter or exit. Also, it accepts the timeout
prop which determines the duration of the animation.
Unlike Transition
, CSSTransition
receives the prop classNames
which allows us to define the classes that should be added based on the different states.
classNames
can be an object or a string. If a string is passed, the class will be used as a prefix for the different states. For example, if you pass to classNames
"fade", the class fade-enter
will be added to the component when it enters. When the component exits, the class fade-exit
is added. The same goes for the rest of the states.
If an object is passed as the value for classNames
, then the keys or properties should be the name of the state, and the value should be the class to apply for that state. For example:
classNames={{
appear: 'fade-in',
appearActive: 'fade-in-active',
appearDone: 'fade-in-appeared',
enter: 'fade-in-enter',
enterActive: 'fade-in-enter-active',
enterDone: 'fade-in-done',
exit: 'fade-out',
exitActive: 'fade-out-active',
exitDone: 'fade-out-active',
}}
Notice that you don't need to add class names for all these states. This just gives you more freedom and flexibility over it. Generally, you should set the class that you want to apply when the element enters to enterActive
, and the class that you want to apply when the element exits to exitActive
. Basically, the active
phase of each state is when you should apply the animation.
So, back to our example, we want to animate an image of a cat as it is toggled by a button. First, we'll add 2 state variables:
const [showCat, setShowCat] = useState(false);
const [imageClasses, setImageClasses] = useState("d-none");
showCat
will be used for the in
prop to determine when the element should enter and exit. As for imageClasses
, we'll get to why we need it later on.
Next, we'll add the CSSTransition
component:
<CSSTransition in={showCat} timeout={500} classNames={{
enterActive: 'animate__bounceIn',
exitActive: 'animate__bounceOut'
}}
onEnter={showImage}
onEntered={removeOpacity}
onExited={hideImage}
className={`animate__animated my-4 ${imageClasses}`}>
...
</CSSTransition>
Notice the following:
- On
enterActive
, which is when the element should appear, we add the classanimate__bounceIn
, and onexitActive
, which is when the element should exit, we add the classanimate__bounceOut
. Both these classes are from the Animate.css library. - We have added a listener for
onEnter
, which will be triggered when the element enters; a listener foronEntered
, which will be triggered when the element has finished entering; a listener foronExited
which will be triggered when the element has exited. We'll implement these listeners in a bit. - We have passed a
className
prop that would add default classes to the child component.
As you can see, we're using the state variable imageClasses
inside the string passed to className
. When using CSSTransition
, you'll assume that the exit state will be applied initially when the initial value passed to in
is false. That's actually not true. Initially, if the value of the in
prop is false, no classes are added.
As we don't want the image to be initially visible, we're using a state variable to add the Bootstrap class d-none
as we're using it in our project. This class will hide the element when added.
And this is why we added the event listeners. We'll change the value of imageClasses
based on each state:
function hideImage() {
setImageClasses("d-none");
}
function showImage(node) {
setImageClasses("d-block");
node.style.opacity = 0;
}
function removeOpacity (node) {
node.style.opacity = 1;
}
Inside CSSTransition
we add the element we want to animate:
<CSSTransition in={showCat} timeout={500} classNames={{
enterActive: 'animate__bounceIn',
exitActive: 'animate__bounceOut'
}}
onEnter={showImage}
onEntered={removeOpacity}
onExited={hideImage}
className={`animate__animated my-4 ${imageClasses}`}>
<img src="https://cataas.com/cat" alt="Cat" />
</CSSTransition>
That's it! The only thing left is to add the button to toggle the showCat
state variable:
<Button onClick={() => setShowCat(!showCat)}>{showCat ? 'Hide' : 'Show'} Cat</Button>
Now, every time you click the button the classes will change based on the state.
Animate a Group of Element
This applies to the first example mentioned in this article. Let's say you have a list and you want to animate whenever an element is added or removed from it. The elements will generally be dynamic, so you can't use CSSTransition
or Transition
on them one by one.
Using TransitionGroup
The component TransitionGroup
wraps a list of CSSTransition
or Transition
components and manages their animation based on their states. In a use case where the list of elements to be added is dynamic, it's useful to use this component.
You pass CSSTransition
or Transition
components as children. There's no need to pass props to TransitionGroup
, as the configuration for the animation is done through the props passed to the children components.
In this example, we'll have an array of 4 elements in the beginning. Then, the user can add an item by clicking on a button or removing an item by clicking on the X icon.
To make the implementation easier, we'll have an array of languages to add items from it randomly:
const defaultLanguages = [
{
id: 1,
name: 'Java'
},
{
id: 2,
name: 'JavaScript'
},
{
id: 3,
name: 'PHP'
},
{
id: 4,
name: 'CSS'
},
{
id: 5,
name: 'C'
},
{
id: 6,
name: 'C#'
},
{
id: 7,
name: 'HTML'
},
{
id: 8,
name: 'Kotlin'
},
{
id: 9,
name: 'TypeScript'
},
{
id: 10,
name: 'Swift'
}
];
And we'll use a one-liner function from 1Loc to get random elements from an array:
const randomItems = (arr, count) => arr.concat().reduce((p, _, __, arr) => (p[0] < count ? [p[0] + 1, p[1].concat(arr.splice((Math.random() * arr.length) | 0, 1))] : p), [0, []])[1];
Then, we'll define a state variable which will be the array of languages we'll show the user in a list:
const [languages, setLanguages] = useState(randomItems(defaultLanguages, 4));
const [counter, setCounter] = useState(11);
We also define a state variable counter
which we'll use to change the id
property from the defaultLanguages
array when adding a new item to the languages
array. This is just to ensure that the IDs are unique when we are choosing random items from the array.
Then, we render a TransitionGroup
component and inside it we loop over the languages
state variable and render a CSSTransition
component for that variable:
<TransitionGroup>
{languages.map(({id, name}) => (
<CSSTransition key={id} classNames={{
enterActive: 'animate__animated animate__lightSpeedInLeft',
exitActive: 'animate__animated animate__lightSpeedOutLeft'
}} timeout={900}>
<li className="p-3 border mb-3 shadow-sm rounded border-info d-flex justify-content-between">
<span>{name}</span>
<CloseButton onClick={() => removeLanguage(id)}></CloseButton>
</li>
</CSSTransition>
))}
</TransitionGroup>
Notice that we're passing the class animate__animated animate__lightSpeedInLeft
for the state enterActive
. As mentioned in the previous section, this class we'll be added when the element enters. We're also passing the class animate__animated animate__lightSpeedOutLeft
for the state exitActive
. As mentioned in the previous section, this class we'll be added when the element exits. Also we're passing the timeout
prop with value 900
.
Inside CSSTransition
we pass the element we want to animate which is an li
element. The element shows the name of the language and has a CloseButton
component which on click should remove the language from the list. Please note that the CloseButton
comes from the React Bootstrap which we're using just for styling purposes.
As you can see TransitionGroup
is only used as a wrapper to these elements.
We also need to add a button to add languages:
<Button onClick={addLanguage}>Add</Button>
What's left is to implement the event listeners addLanguage
and removeLanguage
:
function addLanguage() {
const newLanguages = languages.splice(0);
const newItem = Object.assign({}, randomItems(defaultLanguages, 1)[0]);
newItem.id = counter;
newLanguages.push(newItem);
setLanguages(newLanguages);
setCounter(counter + 1);
}
function removeLanguage (id) {
const newLanguages = languages.splice(0);
const ind = newLanguages.findIndex((language) => language.id === id);
if (ind !== -1) {
newLanguages.splice(ind, 1);
setLanguages(newLanguages);
}
}
The addLanguage
listener picks a random item from the array. We use Object.assign
to clone the item from the array instead of getting the item by reference. We then change the id
to make sure it's unique.
In the removeLanguage
listener we just find the index of the language in the array and remove it.
That's all! If you try it out, items that are added by clicking on the "Add" button will be animated as they enter. Items will also be animated when they exit by clicking on the X icon.
Applying Animation With a Switch
The last case we'll cover is animating something based on its change of state. Let's say we have a button that would toggle between two states, and these two states would require a change in the appearance of another element. For this case, we can use the SwitchTransition
component.
The SwitchTransition
wraps a CSSTransition
or Transition
element. It accepts one prop mode
which can be of two values: out-in
or in-out
, with out-in
being the default. When choosing out-in
, it means that the old state exits first then the new state enters. When choosing in-out
it's the opposite; the new state enters then the old state exits.
When the state of the component changes, the component exits and a new component with the new state enters.
In this example, we'll have an Alert, which is a component exposed by React Bootstrap. We'll have a state that will toggle the variant
, which is the background color and the theme of the Alert component, between danger
and success
. We'll also change the text of the Alert component based on the variant.
First, we'll define the state variable to toggle the state of the Alert component:
const [isDanger, setIsDanger] = useState(true);
Then, we'll render the SwitchTransition
component which will take as a child a CSSTransition
component to manage the animation of the Alert
component:
<SwitchTransition mode="out-in">
<CSSTransition key={isDanger} classNames={{
enterActive: 'animate__animated animate__flipInX',
exitActive: 'animate__animated animate__flipOutX'
}}
timeout={500}>
<Alert variant={isDanger ? 'danger' : 'success'}>{isDanger ? "You're in danger" : "Danger cleared"}</Alert>
</CSSTransition>
</SwitchTransition>
As you can see we pass to SwitchTransition
the mode out-in
, but this is the default mode so it's optional to pass.
For CSSTransition
we pass it the prop key
which will be used to enter and exit elements based on the state. When the state variable isDanger
changes, the component will be removed and a new one with the new value will be added. This key
prop behaves exactly as it would when you render items from an array using map
.
For the enterActive
animation state, we add the class animate__animated animate__flipInX
. For the exitActive
animation sate, we add the class animate__animated animate__flipOutX
.
As for the child of CSSTransition
we pass the Alert
component, which sets the variant
and text based on the value of isDanger
.
Finally, we'll render a button to toggle the value of isDanger
:
<Button onClick={() => setIsDanger(!isDanger)}>
{isDanger ? 'Clear Danger' : 'Bring Danger'}
</Button>
If you try it now, you'll see that when you click the button the Alert will exit and the new one will enter. This is because of the mode out-in
.
If you try to change the mode to in-out
, you'll see that when you click the button a new Alert will enter and then the old one will exit.
Conclusion
Adding animation to components provides a nice user experience and add a flair to your website.
In this tutorial, we learned how to use React Transition Group to animate a component's enterance or exit. Remember, this library does not add the animation for you. This library exposes components that will allow you to add the animation yourself.