Adding functional animations to your app can be a great way to enhance its user experience. When used correctly, animation can help guide the user's attention to certain parts of your app, help re-enforce relationships within the interface, and prevent change blindness.
An example of an animation that can improve the user's experience is fading in an item when it is added to a list. The steps for this animation might look something like this:
- Render the new item.
- Prepare the item for the animation. In this case, set its opacity to
0
. - Transition the opacity of the element from
0
to1
over a period of time.
And for removing the item:
- Mark the item for removal.
- Transition the opacity of the element from
1
to0
over a period of time. - Remove the element once the transition has completed.
Managing all of these states can get cumbersome, so let's try to find a library that can handle it for us. Enter React Transition Group.
React Transition Group contains a set of components that manage the state of a component mounting and un-mounting over time. It doesn't dictate how our components behave as they are mounted or unmounted– that part is up to us. This minimalism gives us the flexibility to define our animations however we want.
In this article we are going to add transition animations to a board of cards, animating cards as they are added to and removed from the board.
Here's the finished result:
You can see a live demo of the animation here.
Prerequisites
You should have a basic understanding of React and CSS transitions.
Packages used
Getting set up
Before we can add animations to our app, we're going to need an app to animate!
The app we're going to be creating is fairly simple as it consists of just 3 components:
-
<Card />
- The component that will be animated in and out of existence.
-
<Board />
- Renders a list of
<Card/>
items.
- Renders a list of
-
<Application />
- The root of our application. Manages the state of cards to be rendered in a
<Board />
, and contains buttons for adding and removing cards.
- The root of our application. Manages the state of cards to be rendered in a
Here's the source code for these components:
<Card/>
function Card ({children, onRemove}) {
return (
<div className="card">
{children}
<button onClick={onRemove}>Remove</button>
</div>
)
}
<Board />
function Board ({children}) {
return (
<ul className="board">
{children}
</ul>
)
}
<Application/>
class Application extends React.Component {
constructor (props) {
super(props)
this.state = {
cards: []
}
this.addCard = this.addCard.bind(this)
this.removeCard = this.removeCard.bind(this)
this.removeLastCard = this.removeLastCard.bind(this)
}
render () {
const {cards} = this.state
return (
<main className="container">
<h1>React Transition Demo</h1>
<button onClick={this.addCard}>Add a card</button>
<button onClick={this.removeLastCard}>Remove a card</button>
<Board>
{
cards.map(card => {
return (
<li className="board__item" key={card.id}>
<Card onRemove={() => {
this.removeCard(card.id)
}}>{card.content}</Card>
</li>
)
})
}
</Board>
</main>
)
}
addCard () {
const {cards} = this.state
const id = cards.length + 1
const newCard = {
id,
content: `Card ${id}`
}
this.setState({
cards: cards.concat([newCard])
})
}
removeCard (id) {
const {cards} = this.state
this.setState({
cards: cards.filter(card => card.id !== id)
})
}
removeLastCard () {
const {cards} = this.state
this.setState({
cards: cards.slice(0, -1)
})
}
}
You can get the styles for these components from GitHub.
If you run this app as-is, you will be able to add and remove cards (exciting stuff!). But the way the cards just pop in and out of existence isn't visually appealing. Let's fix that by adding transition animations.
Adding animations
We want to make adding and removing cards feel seamless. We can do by fading and sliding cards as they are added and removed, like so:
But before we can animate card transitions, we need a way to track the state of cards as they are added and removed from our <Board />
, and run the appropriate animation as cards enter and exit.
The card enter animation should run as soon as a card is added to the list. The card exit animation should run when a card is removed from the list, but the card should remain in the DOM until the animation is finished. Once the animation has completed, the card should be removed from the DOM.
This sounds like a lot of work. So rather than implementing this functionality ourselves, let's use the <TransitionGroup />
component provided by React Transition Group.
Using <TransitionGroup />
<TransitionGroup />
should be wrapped around the list of elements to be animated. So let's replace the <Board />
component in the render method of <Application />
with <TransitionGroup />
.
By default <TransitionGroup />
will wrap its list of child elements in a <span />
, but we can have it wrap our cards in a <Board />
instead by setting the component
prop:
import TransitionGroup from 'react-transition-group/TransitionGroup'
// ...
<TransitionGroup component={Board}>
{
cards.map(card => {
return (
<li className="board__item" key={card.id}>
<Card onRemove={() => {
this.removeCard(card.id)
}}>{card.content}</Card>
</li>
)
})
}
</TransitionGroup>
// ...
But if you run the app and start adding cards, you will notice that cards still pop in and out of existence like before. This is because we haven't yet defined how our cards should behave as they are added or removed. In order to do that, we need to wrap each of our cards in a <Transition />
component.
Using <Transition />
The <Transition />
component from React Transition Group allows us to define how a component should behave when it is rendered or about to be removed from the DOM.
The state of a component being added or removed is handled via an in
prop. This prop is a boolean
value that indicates if the component should be shown or not. A value of true
means the component should be shown, and false
means the component should be hidden.
The value of in
is provided by <TransitionGroup />
, which will set this prop to true
when a component is being added, and to false
when a component is removed.
A change in the value of the in
prop will trigger a series of status changes over a period of time. These status changes allow us to animate a component by applying different styles to it as the status of the transition changes.
We're going to create a <FadeAndSlideTransition />
component that can be used to apply a transition animation to a component as it is mounted and unmounted.
Here is the code for that component:
import Transition from 'react-transition-group/Transition'
// <FadeAndSlideTransition /> is a component that wraps children in
// a <Transition /> component.
// 'children' is the element to be animated.
// 'duration' is the duration of the animation in milliseconds.
// The `in` prop will be provided by <TransitionGroup />.
function FadeAndSlideTransition ({children, duration, in: inProp}) {
// Styles to set on children which are necessary in order
// for the animation to work.
const defaultStyle = {
// Transition "opacity" and "transform" CSS properties.
// Set duration of the transition to the duration of the animation.
transition: `${duration}ms ease-in`,
transitionProperty: 'opacity, transform'
}
// Styles that will be applied to children as the status
// of the transition changes. Each key of the
// 'transitionStyles' object matches the name of a
// 'status' provided by <Transition />.
const transitionStyles = {
// Start with component invisible and shifted up by 10%
entering: {
opacity: 0,
transform: 'translateY(-10%)'
},
// Transition to component being visible and having its position reset.
entered: {
opacity: 1,
transform: 'translateY(0)'
},
// Fade element out and slide it back up on exit.
exiting: {
opacity: 0,
transform: 'translateY(-10%)'
}
}
// Wrap child node in <Transition />.
return (
<Transition in={inProp} timeout={{
// Set 'enter' timeout to '0' so that enter animation
// will start immediately.
enter: 0,
// Set 'exit' timeout to 'duration' so that the 'exited'
// status won't be applied until animation completes.
exit: duration
}}>
{
// Children is a function that receives the current
// status of the animation.
(status) => {
// Don't render anything if component has 'exited'.
if (status === 'exited') {
return null
}
// Apply different styles to children based
// on the current value of 'status'.
const currentStyles = transitionStyles[status]
return React.cloneElement(children, {
style: Object.assign({}, defaultStyle, currentStyles)
})
}
}
</Transition>
)
}
We can apply our fade and slide transition to our cards by wrapping each <Card />
in a <FadeAndSlideTransition />
component:
// render method of <Application />
<TransitionGroup component={Board}>
{
cards.map(card => {
return (
<FadeAndSlideTransition duration={150} key={card.id}>
<li className="board__item">
<Card onRemove={() => {
this.removeCard(card.id)
}}>{card.content}</Card>
</li>
</FadeAndSlideTransition>
)
})
}
</TransitionGroup>
If you rerun the app now, you will see that a nice animation will be applied to cards as they are added and removed from the board.
Here's a break down of how this all works.
Whenever a card is added:
<TransitionGroup />
will render a new<FadeAndSlideTransition />
component, which renders a<Card />
contained within a<Transition />
.Each
<Card />
immediately has itstransition
styles set, which will cause theopacity
andtransform
styles to be animated whenever they are changed.The
in
prop of<FadeAndSlideTransition />
is set totrue
, which causes thechildren
function of the<Transition />
component to be called with a status ofentering
. The styles fromtransitionStyles.entering
are then applied to<Card />
.Because the timeout of the enter animation is set to
0
,children
will be called again immediately with a status ofentered
. This updates the<Card />
'sopacity
andtransform
styles, which triggers a CSS transition.
Whenever a card is removed:
-
<TransitionGroup />
will set thein
prop of the<FadeAndSlideTransition />
component that the card is rendered within tofalse
. - The
children
function of the<Transition />
component will be called with a status ofexiting
.exiting
styles are applied to the<Card />
which causes it to fade out and slide up. - Once the
duration
of the animation has elapsed,children
is invoked with a status ofexited
. We returnnull
in order to remove the<Card />
from the DOM.
Applying inline styles is just one way of creating an animation. You could also use the status
variable from the <Transition />
children
function to apply a CSS class:
<Transition in={inProp} timeout={{
enter: 0,
exit: duration
}}>
{
(status) => {
// Don't render anything if component has "exited".
if (status === 'exited') {
return null
}
return <Card className={`fade fade-${status}`} />
}
}
</Transition>
You would then create a CSS class for each status:
.fade {
transition: ease-in 0.15s;
transition-property: opacity, transform;
}
.fade-entering {
opacity: 0
transform: translateY(-10%);
}
Because <Transition />
only manages the status of our animation, we're free to implement our animation how ever we see fit. Hopefully these two examples are enough for you to get started on a few animations of your own.
If you'd like to see a working example with some code, you can check out the source code for this example on GitHub.
And if you'd like to learn more about React Transition Group, check out the GitHub repo and documentation.
Top comments (4)
Hey Mario, have you found a way to stagger animation of child elements? So if I were to render the card list with, lets say, 10 cards, they would appear one after the other and not all at once.
Any help would be greatly appreciated!
I made a simple React Animation Component:
github.com/bluebill1049/react-simp...
Here is an interactive demo:
react-simple-animate.herokuapp.com/
Finally someone has explained react transition group sufficiently clearly that I can implement it! Thanks so much Mario.
Thanks from me as well!