React Flip Toolkit

Interactive Tutorial

Fork this Code Sandbox to follow along

We're going to build the following animation in response to a user click on any of the cards:

With React-Flip-Toolkit we can build the animation declaratively in about 120 lines of JS

First, let's build out all the components without any animations

That's often a good way to start

import React, { Component } from "react"
const listData = [...Array(3).keys()]
const ListItem = ({ index, onClick }) => (
<div className="listItem" onClick={() => onClick(index)}>
<div className="listItemContent">
<div className="avatar" />
<div className="description">
{listData.map(i => (
<div key={i} />
))}
</div>
</div>
</div>
)
const ExpandedListItem = ({ index, onClick }) => (
<div className="expandedListItem" onClick={() => onClick(index)}>
<div className="expandedListItemContent">
<div className="avatar avatarExpanded" />
<div className="description">
{listData.map(i => (
<div key={i} />
))}
</div>
<div className="additional-content">
{listData.map(i => (
<div key={i} />
))}
</div>
</div>
</div>
)
export default class AnimatedList extends Component {
state = { focused: null }
onClick = index =>
this.setState({
focused: this.state.focused === index ? null : index
})
render() {
return (
<div className="staggered-list-content">
<ul className="list">
{listData.map(index => {
return (
<li key={index}>
{index === this.state.focused ? (
<ExpandedListItem
index={this.state.focused}
onClick={this.onClick}
/>
) : (
<ListItem index={index} key={index} onClick={this.onClick} />
)}
</li>
)
})}
</ul>
</div>
)
}
}
Here's our first of three components, a simple ListItem

Next, let's add a basic FLIP animation with React-Flip-Toolkit

If you need a refresher on what FLIP animations are

check out this article

import React, { Component } from "react"
import { Flipper, Flipped } from "react-flip-toolkit"
const listData = [...Array(3).keys()]
const ListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`}>
<div className="listItem" onClick={() => onClick(index)}>
<div className="listItemContent">
<div className="avatar" />
<div className="description">
{listData.map(i => (
<div key={i} />
))}
</div>
</div>
</div>
</Flipped>
)
const ExpandedListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`}>
<div className="expandedListItem" onClick={() => onClick(index)}>
<div className="expandedListItemContent">
<div className="avatar avatarExpanded" />
<div className="description">
{listData.map(i => (
<div key={i} />
))}
</div>
<div className="additional-content">
{listData.map(i => (
<div key={i} />
))}
</div>
</div>
</div>
</Flipped>
)
export default class AnimatedList extends Component {
state = { focused: null }
onClick = index =>
this.setState({
focused: this.state.focused === index ? null : index
})
render() {
return (
<Flipper
flipKey={this.state.focused}
className="staggered-list-content"
spring="gentle"
>
<ul className="list">
{listData.map(index => {
return (
<li key={index}>
{index === this.state.focused ? (
<ExpandedListItem
index={this.state.focused}
onClick={this.onClick}
/>
) : (
<ListItem index={index} key={index} onClick={this.onClick} />
)}
</li>
)
})}
</ul>
</Flipper>
)
}
}
Import the Flipper and Flipped components

Now, the transition between ListItem and ExpandedListItem will automatically be tweened.

It looks...okay

Next up

We need to add a counter transform to the parent to prevent warping

import React, { Component } from "react"
import { Flipper, Flipped } from "react-flip-toolkit"
const listData = [...Array(3).keys()]
const ListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`}>
<div className="listItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="listItemContent">
<div className="avatar" />
<div className="description">
{listData.map(i => (
<div />
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
const ExpandedListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`}>
<div className="expandedListItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="expandedListItemContent">
<div className="avatar avatarExpanded" />
<div className="description">
{listData.map(i => (
<div />
))}
</div>
<div className="additional-content">
{listData.map(i => (
<div key={i} />
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
export default class AnimatedList extends Component {
state = { focused: null }
onClick = index =>
this.setState({
focused: this.state.focused === index ? null : index
})
render() {
return (
<Flipper
flipKey={this.state.focused}
className="staggered-list-content"
spring="gentle"
>
<ul className="list">
{listData.map(index => {
return (
<li key={index}>
{index === this.state.focused ? (
<ExpandedListItem
index={this.state.focused}
onClick={this.onClick}
/>
) : (
<ListItem index={index} key={index} onClick={this.onClick} />
)}
</li>
)
})}
</ul>
</Flipper>
)
}
}
Add another Flipped component to wrap the inner contents of ListItem

By using the inverseFlipId prop, we've totally canceled the effects of the parent transform on the children

Now, we can add FLIP animations to the children so they animate smoothly as well

import React, { Component } from 'react'
import { Flipper, Flipped } from 'react-flip-toolkit'
const listData = [...Array(3).keys()]
const ListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`}>
<div className="listItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="listItemContent">
<Flipped flipId={`avatar-${index}`}>
<div className="avatar" />
</Flipped>
<div className="description">
{listData.map(i => (
<Flipped flipId={`description-${index}-${i}`} key={i}>
<div />
</Flipped>
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
const ExpandedListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`}>
<div className="expandedListItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="expandedListItemContent">
<Flipped flipId={`avatar-${index}`}>
<div className="avatar avatarExpanded" />
</Flipped>
<div className="description">
{listData.map(i => (
<Flipped flipId={`description-${index}-${i}`} key={i}>
<div />
</Flipped>
))}
</div>
<div className="additional-content">
{listData.map(i => (
<div key={i} />
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
export default class AnimatedList extends Component {
state = { focused: null }
onClick = index =>
this.setState({
focused: this.state.focused === index ? null : index
})
render() {
return (
<Flipper
flipKey={this.state.focused}
className="staggered-list-content"
spring="gentle"
>
<ul className="list">
{listData.map(index => {
return (
<li key={index}>
{index === this.state.focused ? (
<ExpandedListItem
index={this.state.focused}
onClick={this.onClick}
/>
) : (
<ListItem index={index} key={index} onClick={this.onClick} />
)}
</li>
)
})}
</ul>
</Flipper>
)
}
}
Wrap the children of ListItem in their own Flipped components

Unfortunately, now the children of the list items are animating even when we don't want them to

It doesn't look terrible but it could be bad for performance if we're animating elements unnecessarily

We need a way to tell the Flipped component to only animate in certain situations

import React, { Component } from "react"
import { Flipper, Flipped } from "react-flip-toolkit"
const listData = [...Array(3).keys()]
const shouldFlip = index => (prevDecisionData, currentDecisionData) =>
index === prevDecisionData || index === currentDecisionData
const ListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`} shouldInvert={shouldFlip(index)}>
<div className="listItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="listItemContent">
<Flipped flipId={`avatar-${index}`} shouldFlip={shouldFlip(index)}>
<div className="avatar" />
</Flipped>
<div className="description">
{listData.map(i => (
<Flipped
flipId={`description-${index}-${i}`}
shouldFlip={shouldFlip(index)}
>
<div />
</Flipped>
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
const ExpandedListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`}>
<div className="expandedListItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="expandedListItemContent">
<Flipped flipId={`avatar-${index}`}>
<div className="avatar avatarExpanded" />
</Flipped>
<div className="description">
{listData.map(i => (
<Flipped flipId={`description-${index}-${i}`} key={i}>
<div />
</Flipped>
))}
</div>
<div className="additional-content">
{listData.map(i => (
<div key={i} />
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
export default class AnimatedList extends Component {
state = { focused: null }
onClick = index =>
this.setState({
focused: this.state.focused === index ? null : index
})
render() {
return (
<Flipper
flipKey={this.state.focused}
className="staggered-list-content"
spring="gentle"
decisionData={this.state.focused}
>
<ul className="list">
{listData.map(index => {
return (
<li key={index}>
{index === this.state.focused ? (
<ExpandedListItem
index={this.state.focused}
onClick={this.onClick}
/>
) : (
<ListItem index={index} key={index} onClick={this.onClick} />
)}
</li>
)
})}
</ul>
</Flipper>
)
}
}
We can pass in a decisionData prop to Flipper with some data that will help Flipped components decide whether to animate

Finally, let's add in some stagger to the transitions so that the movement looks more natural

import React, { Component } from "react"
import { Flipper, Flipped } from "react-flip-toolkit"
const listData = [...Array(3).keys()]
const shouldFlip = index => (prev, current) =>
index === prev || index === current
const ListItem = ({ index, onClick }) => (
<Flipped
flipId={`listItem-${index}`}
stagger="card"
shouldInvert={shouldFlip(index)}
>
<div className="listItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="listItemContent">
<Flipped
flipId={`avatar-${index}`}
stagger="card-content"
shouldFlip={shouldFlip(index)}
>
<div className="avatar" />
</Flipped>
<div className="description">
{listData.slice(0, 3).map(i => (
<Flipped
flipId={`description-${index}-${i}`}
stagger="card-content"
shouldFlip={shouldFlip(index)}
>
<div />
</Flipped>
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
const ExpandedListItem = ({ index, onClick }) => (
<Flipped flipId={`listItem-${index}`} stagger="card">
<div className="expandedListItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="expandedListItemContent">
<Flipped flipId={`avatar-${index}`} stagger="card-content">
<div className="avatar avatarExpanded" />
</Flipped>
<div className="description">
{listData.slice(0, 3).map(i => (
<Flipped
flipId={`description-${index}-${i}`}
stagger="card-content"
>
<div />
</Flipped>
))}
</div>
<div className="additional-content">
{listData.slice(0, 3).map(i => (
<div key={i} />
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
export default class AnimatedList extends Component {
state = { focused: null }
onClick = index =>
this.setState({
focused: this.state.focused === index ? null : index
})
render() {
return (
<Flipper
flipKey={this.state.focused}
className="staggered-list-content"
spring="gentle"
staggerConfig={{
card: {
reverse: this.state.focused !== null,
speed: 0.5
}
}}
decisionData={this.state.focused}
>
<ul className="list">
{listData.map(index => {
return (
<li key={index}>
{index === this.state.focused ? (
<ExpandedListItem
index={this.state.focused}
onClick={this.onClick}
/>
) : (
<ListItem index={index} key={index} onClick={this.onClick} />
)}
</li>
)
})}
</ul>
</Flipper>
)
}
}
Add stagger props to the Flipped components with unique string ids identifying which stagger group they belong to

And we're done!

Well, almost

We need to add the fade in transition for the contents of the ExpandedListItem

Since it's a simple fade in effect, we can just toggle a CSS class rather than using FLIP

import React, { Component } from "react"
import { Flipper, Flipped } from "react-flip-toolkit"
const listData = [...Array(3).keys()]
const shouldFlip = index => (prev, current) =>
index === prev || index === current
const ListItem = ({ index, onClick }) => (
<Flipped
flipId={`listItem-${index}`}
stagger="card"
shouldInvert={shouldFlip(index)}
>
<div className="listItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="listItemContent">
<Flipped
flipId={`avatar-${index}`}
stagger="card-content"
shouldFlip={shouldFlip(index)}
>
<div className="avatar" />
</Flipped>
<div className="description">
{listData.slice(0, 3).map(i => (
<Flipped
flipId={`description-${index}-${i}`}
stagger="card-content"
shouldFlip={shouldFlip(index)}
>
<div />
</Flipped>
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
const ExpandedListItem = ({ index, onClick }) => (
<Flipped
flipId={`listItem-${index}`}
stagger="card"
onStart={el => {
setTimeout(() => {
el.classList.add("animated-in")
}, 600)
}}
>
<div className="expandedListItem" onClick={() => onClick(index)}>
<Flipped inverseFlipId={`listItem-${index}`}>
<div className="expandedListItemContent">
<Flipped flipId={`avatar-${index}`} stagger="card-content">
<div className="avatar avatarExpanded" />
</Flipped>
<div className="description">
{listData.slice(0, 3).map(i => (
<Flipped
flipId={`description-${index}-${i}`}
stagger="card-content"
>
<div />
</Flipped>
))}
</div>
<div className="additional-content">
{listData.slice(0, 3).map(i => (
<div key={i} />
))}
</div>
</div>
</Flipped>
</div>
</Flipped>
)
export default class AnimatedList extends Component {
state = { focused: null }
onClick = index =>
this.setState({
focused: this.state.focused === index ? null : index
})
render() {
return (
<Flipper
flipKey={this.state.focused}
className="staggered-list-content"
spring="gentle"
staggerConfig={{
card: {
reverse: this.state.focused !== null,
speed: 0.5
}
}}
decisionData={this.state.focused}
>
<ul className="list">
{listData.map(index => {
return (
<li key={index}>
{index === this.state.focused ? (
<ExpandedListItem
index={this.state.focused}
onClick={this.onClick}
/>
) : (
<ListItem index={index} key={index} onClick={this.onClick} />
)}
</li>
)
})}
</ul>
</Flipper>
)
}
}
The onStart callback on the Flipped component allows us to do some work at the beginning of a FLIP animation

You can check out the final code in this Code Sandbox

Thanks for making it to the final slide

Here's another link to React Flip Toolkit

Credits