Implementing material design animation on cards list using React.js

Ori Harel
Capriza Engineering
5 min readMay 12, 2017

Our app, WorkSimple (iOS, Android), is a hybrid app written in Javascript, HTML and CSS. We recently moved from Angular 1.5 to React and on the road we encountered lots of interesting challenges, each of those probably deserves its own post, but for now I’d like to focus on animation.

I happen to be a big fan of material design. I think if you take the time to follow the very well written guidelines you’ll find it very readable and it will make a whole lot of sense to you. I recently had the need to implement a cards list, and immediately went there for inspiration. I really liked what they are spec’ing for cards, cards list and particularly animations.

I also happen to be a big fan of React. The component system that they built fits nicely with my UI building philosophy — UI should be constructed of components with a well defined lifecycle that can be easily reused across the entire app.

One thing that I found a bit challenging though was creating smooth animations that fit well with the “don’t-touch-the-dom” approach of react. This approach is also known as the “everything-should-be-realized-using-state”.

Before we moved to React, I used to write animations that are based on DOM events and css (css transitions, Keyframe animations). But when in React, one is highly encouraged, if not forced to, rely only on state of the components and describe the UI in a more abstract way than just plain HTML.

Of course, you can go ahead and try the official documentation regarding animations (only to find out that it’s deprecated and politely asked to navigate to another repo). Nevertheless, I tried that and found out it fell short when trying to achieve what I originally set out to implement, which is the cards list, and more specifically, card deleting animation.

When the user deletes a card (and for the sake of this example just by tapping it) the card fades away, and the rest of the cards move up nicely to fill in the missing gap the just-deleted-card left. Check out the demo.

Material design cards list

Ok, let’s talk about code now. For this example, I only have 2 important javascript files: CardsList.js and Card.js (besides the auto-generated App.js and index.js from create-react-app).

Card.js will only take care of it’s own ‘fade out’ animation and is going to be a stateless component (some folks refer to those as ‘stupid components’), while CardsList.js will take care of moving the rest of the cards to fill in the gap and actually update the state. Note that in this example, for simplicity, I avoided having real network calls since I wanted to focus on animations.

Here’s CardsList.js render method:

export default class CardsList extends Component {   
...
render() {
return <div className="cards-list">
{this.state.cards.map((card, i)=>{
return <Card
key={card.id}
card={card}
moveRestUp={this.moveRestUp}
resetTravel={this.resetTravel}
beginRemoveCard={this.beginRemoveCard}/>
})}
</div>
}
}

In this component, we render a div with class name “cards-list”. Inside this div element we loop through a list of cards (this.state.cards) and for each one we return a Card component. We also add some props to it, some of them are functions that I will shortly cover. I also avoided state management libraries such as Redux and Mobx for now…

Here is Card.js:

export default class Card extends Component {
...
getStyleObject(travelTo){
const {card} = this.props;
let opacity = card.removing ? 0 : 1,
transform = `translate3d(0,${travelTo}px,0)`;

return {opacity, transform}
}

render() {
const {card} = this.props;
let travelTo = card.travelTo || 0, className = 'card';
if (travelTo !== 0) className += ' traveling';
return (
<div className={className}
style={this.getStyleObject(travelTo)}
ref={ref => this.cardRef = ref}
onClick={this.onCardClick.bind(this)}
id={card.id}>
<div className='card-inner ripple'>
<div className="information">
<img .../>
<div className="value">{card.value}</div>
<div className="label">{card.label}</div>
</div>
</div>
</div>
)
}
}

Initially, cards are just rendered one after the other with an image and two text elements (value and label). The fun starts when you click on a card, then the onClick handler kicks in (onCardClick) invoking a function on the parent CardsList component. This, in turn, changes the state of the card in a way that affects the opacity, causing the card to fade away .Note that card has ‘hard coded’ opacity transition defined in index.css. But the Card component also registers a handler for this opacity transition, for when it’s done, it will invoke another parents function, moveRestUp that changes the states of all the following cards in the list which will control a translate3d property (travelTo).

Here’s Card.js onCardClick method:

onCardClick() {

let delta = ...//calculate the top position of the card
const {card} = this.props;
this.props.beginRemoveCard(card);
this.cardRef.addEventListener('transitionend', (e)=>{
if (e.propertyName === 'opacity')
this.props.moveRestUp(this.props.card, delta);
});
}

And here’s the methods on CardsList.js:

beginRemoveCard(card) {
let newCards = [...this.state.cards];
newCards[newCards.indexOf(card)].removing = true;
this.setState({cards: newCards});
}

moveRestUp(card, delta){
let initialCardIndex = this.state.cards.indexOf(card);
let newCards = [...this.state.cards];
newCards.forEach((currCard, i)=>{
if (i>initialCardIndex){
currCard.travelTo = delta
}
});

this.setState({cards: newCards});
}

Lastly, the last card that is transition up, is responsible to ‘tell’ the parent that all the cards are in their new position, and now it’s time to actually update the list. By invoking the CardsList’s function resetTravel the state is updated (removeCard) and the travelTo property is deleted.

resetTravel(){
let newCards = this.removeCard(this.state.cards.find(card=>card.removing));

if (newCards) {
newCards.forEach(card=>delete card.travelTo);
this.setState({cards: newCards});
}
}

That’s it. Although pretty simple and straightforward React code here, it was a nice exercise for me trying to adapt to the new way of writing apps. This is a great example of how an implementation that used to be an imperative one (various DOM mutations scattered all over the code) could now be declarative one (every frame in the app can be declared by a javascript object — the state).

Code can be found in this Github repo.

--

--

Ori Harel
Capriza Engineering

Engineering Manager, love startups, love NBA Basketball but mostly procrastinate. Work @ Taranis