Arwes LogotypeArwes Logotext

Animation

Assemble and disassemble user interfaces using animation controls in React.

Based on the UI/UX guidelines, the animation tools can be used to manage and control animation flows in applications. Then the specific components animations or components events can be defined more easily, such as visual effects or sound playbacks when a component transition from one state to another.

The animation tools require the React component tree to allow the dynamic communication between components.

It goes hand in hand with the sounds tools, though it is not a dependency.

Installation

All the tools are bundled and can be installed with the following NPM package:

bash
npm install @arwes/animation

The animation management tooling requires React v17 as a peer-dependency.

bash
npm install react@17
npm install react-dom@17

Animators

Any React component can have animation controls and be added to the Arwes system by using the <Animator/> component.

The <Animator/> component will work as an animated node in the system. It will be interconnected by sharing data and communicating with its parent and children counterparts.

Normally, the withAnimator higher-order component (HOC) will handle the animator component setup instead of the component itself. So class/function components with specific initial settings can be created.

A HTML button component can be animated like:

tsx
import React, { FC, useRef, MutableRefObject } from 'react';
import { AnimatorRef, AnimatorClassSettings, withAnimator } from '@arwes/animation';
interface ButtonProps {
// An animated component will receive the `animator` prop with
// the animator API.
animator: AnimatorRef
}
const ButtonComponent: FC<ButtonProps> = props => {
const buttonRef = useRef<HTMLButtonElement>(null);
// Here you can set any kind of data you want to pass to the
// animator "animate hooks". In this case, a HTML button ref.
props.animator.setupAnimateRefs(buttonRef);
return (
<button ref={buttonRef}>
{props.children}
</button>
);
};
// "enter" is the duration to transition from exited to entering to entered.
// "exit" is the duration to transition from entered to exiting to exited.
const duration = { enter: 200, exit: 200 };
const useAnimateEntering = (
animator: AnimatorRef,
buttonRef: MutableRefObject<HTMLButtonElement>
): void => {
// On flow entering:
// You can animate the references passed as the second parameter.
};
const useAnimateExiting = (
animator: AnimatorRef,
buttonRef: MutableRefObject<HTMLButtonElement>
): void => {
// On flow exiting:
// You can animate the references passed as the second parameter.
};
const animatorClassSettings: AnimatorClassSettings = {
duration,
useAnimateEntering,
useAnimateExiting
};
const Button = withAnimator(animatorClassSettings)(ButtonComponent);
export { ButtonProps, Button };

The new wrapped component <Button/> by the HOC will have animations enabled and it will handle when to call the animate hooks on flow transitions.

But this component will not execute any HTML/CSS animations on its elements, so we need to check how to add them in the animate hooks.

Animate Hooks

An animator will only handle the flow transitions and flow control management. The actual HTML elements animations should be handled inside the animate hooks.

By setting up the references to the HTML elements inside the component render with props.animator.setupAnimateRefs(), the hooks can receive them for manipulations.

The HTML elements animations can be made with any HTML animation library. Recommended libraries are animejs and GreenSock.

If the example <Button/> component should have animations:

  • To enter the flow with opacity from 0 to 1 and time duration.enter.
  • To exit the flow with opacity from 1 to 0 and time duration.exit.

They could be written like this using animejs:

tsx
import anime from 'animejs';
// ...
const useAnimateEntering = (
animator: AnimatorRef,
buttonRef: MutableRefObject<HTMLButtonElement>
): void => {
anime({
targets: buttonRef.current,
duration: animator.duration.enter,
easing: 'linear',
opacity: [0, 1]
});
};
const useAnimateExiting = (
animator: AnimatorRef,
buttonRef: MutableRefObject<HTMLButtonElement>
): void => {
anime({
targets: buttonRef.current,
duration: animator.duration.exit,
easing: 'linear',
opacity: [1, 0]
});
};
// ...

When the flow enters, it would go from hidden to visible. When the flow exits, it will reverse it.

There is a hook for all the flow states transitions and the component mounting and unmounting lifecycles.

  • useAnimateMount - On component mount.
  • useAnimateEntering - On flow entering.
  • useAnimateEntered - On flow entered.
  • useAnimateExiting - On flow exiting.
  • useAnimateExited - On flow exited.
  • useAnimateUnmount - On component unmount.

Now, according to how the component is used, it will behave in a certain way.

Parent/Children Nodes

By default, if the component is used alone or if it does not have parent counterparts, then it is considered a root component, unless configured otherwise.

If the component is a root node, on component mount, by default its flow will transition from exited to entering to entered and stay there.

If the component is a child node, it will always listen to its closest parent node flow state to know when it should transition. See more in Nesting Animators.

Normally, the root nodes are managed using its instance settings, and the children nodes are delegated to its parents nodes.

Instance Settings

An animated component when it is used, it can receive an animator settings object to configure declaratively its animator.

The root nodes can receive a the property animator.activate to determine when it should enter or exit the animation flow. This in turn will manage its delegated children nodes accordingly.

In the example of the <Button/> component, it could receive instance settings like:

tsx
import { AnimatorInstanceSettings, ... } from '@arwes/animation';
// ...
const instanceSettings: AnimatorInstanceSettings = {
// true to make the component transition in.
// false to make the component transition out.
activate: true
};
// ...
<Button animator={instanceSettings}>
My Animated Button
</Button>
// ...

A complete example using the <Button/> component as a root node can look like this:

JSX

It will update the component flow activation every 2 seconds.

Since it is an animated component, by default, its flow will be exited. Then if it is activated, it will transition from exited to entering to entered. When it transitions to entering, the hook useAnimateEntering will be called.

When it is deactivated, it will transition from entered to exiting to exited. When it transitions to exiting, the hook useAnimateExiting will be called.

The component should set the initial styles of the animator animations changes. In this case, the component is set to opacity: 0 when it is exited in the animation. Since its initial value is exited and the hooks are not executed, the component should set the initial CSS styles to opacity: 0 too for consistency.

When an animated components exits, it is not unmounted.

When nesting animators, the children nodes will listen to their parent nodes so there is not management needed for them, it would be automatically handled by the animation tools.

General Settings

Usually, the application animated components would have certain similar settings for consistency. The available general settings can be provided using the <AnimatorGeneralProvider/> provider.

For example, to provide common animator durations to all animated children nodes, something like this can be done:

tsx
import React, { FC } from 'react';
import { AnimatorGeneralProvider, AnimatorGeneralProviderSettings } from '@arwes/animation';
const animatorGeneralSettings: AnimatorGeneralProviderSettings = {
duration: { enter: 200, exit: 100 }
};
// ...
<AnimatorGeneralProvider animator={animatorGeneralSettings}>
{/* ... */}
</AnimatorGeneralProvider>
// ...

Suspend On Visibility Change

Some HTML animation libraries like animejs pause the animations when the browser window looses focus. In animejs this behavior can be disabled using:

tsx
import anime from 'animejs';
anime.suspendWhenDocumentHidden = false;

You can see more at anime - suspend on visibility change.


You can see and play with more examples in the playground.