React Animations with React Motion

Cheng Lou's React Motion brings a new way to make animations for React apps. I watched his video but did not catch the idea for the first time. I skipped because none of my projects need animations at that point.

But I have to face it now.

I started a new personal project which will build an online version of KDE Plasma 5 Desktop Environment recently. The login screen is the first interface to be built.

login

The journey to create the interface above proves that it's very easy to make animations with React Motion.

Living Demo of React&Redux based Login Screen

React Motion exports three main components, Motion, StaggeredMotion and TransitionMotion. In this blog, we’ll be taking a look at the Motion component, the one you will find yourself using most of the time.

How does React Motion work

If you have learned/used CSS Animations before, you may think today's React Motion should use Transition, Transform and Animations from CSS3.

Stop think like that. React Motion works like what jQuery's "animate" function does, and its idea is very straightforward in fact.

Think about how to scale an icon as the gif shows above in pure css.

<div class="icon">  
</div>  
.icon {
  transition: 0.8s;
}

.icon.active {
  transform: scale(2);
}

That's simple, right? But do not try to find a "transition" property in React Motion like I did. That's will not be a good experience I swear. We can still use "transform", but React Motion implements its own "transition".

import { Motion, spring } from 'react-motion';

export default function Icon({ active }) {  
  <Motion style={{ x: spring(active? 2 : 1) }}>
   {({ x }) =>
     <div class="icon" style={{ scale: `${x}` }}></div>
   }
  </Motion>
}

The original icon markup is wrapped with Motion component. If you open Chrome Dev Tools and check the style of the icon, you will find that the value of the "scale" property of the icon moves slowly from 1 to 2 (a series numbers such like 1, 1.11, 1.12, 1.13 ... 1.98, 2) when the "active" prop changes from false to true. "x" is just a normal javascript variable. It helps get the numbers between 1 and 2 (there may be numbers larger than 2 or smaller than 1, check spring 's document if you are curious about that). The spring function from React Motion does the trick.

Implement the Animations in the Login Screen

It's easy to find that there are three different animations in the Login Screen. Scaling the icon, Fading the icon and name, and Sliding the Accounts

Scaling the icon && Fading the icon and name

export default function Account({ account, active, onClick }) {  
  const logoClass = active ? styles.activeLogoImage : styles.logoImage;
  const logoContainerClass = active ? styles.activeLogoContainer : styles.logoContainer;
  return (
    <Motion style={{ x: spring(active ? 1 : 0.5) }}>
      {({ x }) =>
        <div className={styles.account} onClick={onClick} style={{ opacity: `${x}` }}>
          <div className={styles.wrapper}>
            <Motion style={{ y: spring(active ? 1 : 0.85) }}>
              {({ y }) =>
                <div className={logoContainerClass} style={{ transform: `scale(${y})` }}>
                  <img className={logoClass} src={account.icon} />
                </div>
              }
            </Motion>
          </div>
          <div className={styles.name}>{account.name}</div>
        </div>
      }
    </Motion>
  );
}

As the code above shows, we scale the icon and fade the icon and name separately. Nothing fancy.

Sliding the Accounts

Check the gif at the beginning again. There is only one active Account at one point, and the list of Accounts will slide left or right to translate the active Account to index 1 (zero based).

We need to know how long should translate when the active Account changes.
accounts-slide-animation-calculate

calcTranslateX(activeAccountIndex) {  
  const fixedIndex = 1;
  const accountWidth = 84;
  const accountGutter = 30;
  const diff = fixedIndex - activeAccountIndex;

  return diff * (accountWidth + accountGutter);
}

accountWidth is the width of one Account, accountGutter is the space between Accounts.

export default class Accounts extends Component {  
  constructor(props) {
    super(props);
    this.handleAccountClick = this.handleAccountClick.bind(this);
  }

  handleAccountClick(account) {
    this.props.onAccountClick(account);
  }

  getActiveAccountIndex(accounts) {
    return accounts.findIndex(account => !!account.active);
  }

  calcTranslateX(activeAccountIndex) {
    const fixedIndex = 1;
    const accountWidth = 84;
    const accountGutter = 30;
    const diff = fixedIndex - activeAccountIndex;

    return diff * (accountWidth + accountGutter);
  }

  render() {
    const { accounts } = this.props;
    const index = this.getActiveAccountIndex(accounts);
    const translateX = this.calcTranslateX(index);

    return (
      <Motion style={{ x: spring(translateX) }}>
        {({ x }) =>
          <div className={styles.wrapper}>
            <div className={styles.accounts} style={{ transform: `translateX(${x}px)` }}>
              {accounts.map(a =>
                (
                  <div className={styles.account} key={a.id}>
                    <Account
                      active={a.active}
                      account={a}
                      onClick={() => this.handleAccountClick(a)}
                    />
                  </div>
                ))
              }
            </div>
          </div>
        }
      </Motion>
    );
  }

The code is here if you are interested.

Luo Gang

Read more posts by this author.