A step-by-step guide that goes through every aspect of react-animatronics.
- 01. Basics
- 02. Higher-Order Components
- 03. Multi-Phase Animations
- 04. Animating Attributes Simultaneously
- 05. Animating Components Simultaneously
- 06. Named Animations
- 07. Accessing DOM Nodes
- 08. Debug Panel
- 09. Spring Animations
- 10. Custom Easing
- 11. Delays
- 12. Animatable Values
React-animatronics provides two components: <Animatronics>
and <Control>
.
Both components take a FaCC, a single child that must be a function.
React Tip: "Functions as Child Components (FaCC)" is a common React pattern for component encapsulation/reusability. It goes by a few other aliases, most notably "render props". If this is the first time you're hearing about this, here are some articles:
Here's a non-working code example just showing how you'd use the components:
import { Animatronics, Control } from 'react-animatronics'
const animations = [/* intentionally blank for now */];
const App = () => (
<Animatronics animations={ animations }>{
({ playAnimation }) => (
<Control name='square'>{
({ animatronicStyles }) => (
<div
style={{
backgroundColor: 'blue',
height: '100px',
width: '100px',
cursor: 'pointer',
position: 'absolute',
...animatronicStyles
}}
onClick={() => playAnimation()}
/>
)
}</Control>
)
}</Animatronics>
);
Let's start with the <Animatronics>
component.
The <Animatronics>
component is the "animation runner". It provides functions
and context to its children that allow you to run animations involving any
controlled component in its decendants.
The <Animatronics>
component requires an animations
prop that we'll
describe in the last part of this section. For now, let's focus on the
playAnimation
prop it passes into its child. When you call playAnimation
,
react-animatronics will execute the animations
you've declared.
If you need to execute logic after playAnimation
has completed, you can either
pass a callback or call .then
on the returned promise:
playAnimation(() => {
/* execute logic */
});
playAnimation().then(() => {
/* execute logic */
});
In addition to playAnimation
, the child also gets cancelAnimation
and reset
:
<Animatronics animations={ animations }>{
({ playAnimation, cancelAnimation, reset }) => ()
}</Animatronics>
The cancelAnimation
function will stop currently running animations.
The reset
function will stop running animations and remove any styles that
react-animatronics has applied to your components.
Now let's look into the <Control>
component.
The <Control>
component works in tandem with <Animatronics>
. In fact, it must
have an <Animatronics>
component as its ancestor.
The <Control>
component makes its child a controlled component by registering
the child with its <Animatronics>
ancestor.
<Control>
takes a single name
prop that must be a string which uniquely
identifies its child component. The name
prop is important when declaring
your animations
(more on this later).
<Control>
will pass a single animatronicStyles
prop to it child. The
animatronicStyles
is an object that contains the interpolated values for your
animation.
An important, performance-related note. When an animation is running, the
<Control>
component will update its child with new animatronicStyles
on
every animation frame (60 frames per second is approximately one frame every 17
milliseconds). If you're rendering expensive components within a <Control>
,
you'll likely want to define shouldComponentUpdate()
or reorganize your
components such that the expensive parts are outside of <Control>
.
Finally, let's look into how to define the animations
you pass into <Animatronics>
.
In its most basic form, the animations
prop can be an array of objects:
const animations = [
{
square: {
duration: 500,
from: { left: '0px' },
to: { left: '200px' }
}
}
]
Each element in the array describes a single animation phase. In the snippet above, we've declared an animation with a single phase. We'll go into declaring more than one phase in the Multi-Phase Animations section, but we're going to keep things simple in this section.
That single phase describes how the controlled component named "square"
should be animated.
When we call playAnimation()
, that component will receive an updated
animatronicStyles
prop on every animation frame over the course of 500
milliseconds.
That's it for the basics! Next up, a short detour into an alternative, HoC API.
For those who would rather use higher-order components instead of FaCCs,
react-animatronics provides two HoCs: withAnimatronics
and withControl
.
React Tip: "Higher-Order Components (HoC)" is another common React pattern for component encapulation/reusability. More info:
The withAnimatronics
and withControl
functions work in very much the same
way as their component counterparts:
import { withAnimatronics, withControl } from 'react-animatronics'
const animations = [/* intentionally blank for simplicity */];
const Square = ({ playAnimation, animatronicStyles }) => (
<div
style={{
backgroundColor: 'blue',
height: '100px',
width: '100px',
cursor: 'pointer',
position: 'absolute',
...animatronicStyles
}}
onClick={() => playAnimation()}
/>
);
const Controlled = withControl('square')(Square);
const App = withAnimtronics(animations)(Controlled);
In fact, both HoCs are implemented in terms of their component counterparts.
The withAnimatronics
function expects a single animations
argument that
will internally be passed as a prop to <Animatronics>
. It will pass the
playAnimation
, cancelAnimation,
and reset
props into its wrapped
component.
The withControl
function expects a single name
argument that will
internally be passed as a prop to <Control>
. It will pass the
animatronicStyles
prop into its wrapped component.
I won't go into the pros and cons of HoCs vs FaCCs. I think there's a time and
place for both, which is why they're both in react-animatronics. In the rest
of this walkthrough, I'll be referring to <Animatronics>
and <Control>
but they'll always be interchangeable with withAnimatronics
and withControl
,
respectively.
That's it! Next up, declaring a sequence of animation phases.
The title "Multi-Phase Animations" sounds more fancy than the concept it describes. Here's what it is:
const animations = [
{
square: {
duration: 500,
from: { left: '0px' },
to: { left: '200px' }
}
},
{
square: {
duration: 500,
from: { top: '0px' },
to: { top: '200px' }
}
}
];
Instead of declaring a single phase, we've now declared two. When you call
playAnimation
, the "square"
controlled component will first animate its
left
, and then its top
. The phases occur in sequence, one after the other.
That's it! Next up, animating multiple attributes simultaneously.
In the previous animation
examples, we've only declared a single
attribute in from
and to
, but you can declare multiple attributes:
const animations = [
{
square: {
duration: 500,
from: {
left: '0px',
top: '0px'
},
to: {
left: '200px',
top: '200px'
}
}
}
];
This describes an animation that will animate the left
and top
attributes
at the same time such that the "square"
component will move diagonally down to
the right.
But that's not all! There's another way to animate attributes simultaneously.
Sometimes you'll want to animate different attributes in different ways while
still animating them simultaneously. To so this, you can declare animations
with a slightly different format:
const animations = [
{
square: [
{
duration: 500,
from: { left: '0px' },
to: { left: '200px' },
},
{
duration: 300,
from: { top: '0px' },
to: { top: '200px' },
}
]
}
];
Previously, we declared the value for square
as an object, but now we've declared
it as an array. Each object in the array describes an animation that will occur in
parallel with the other animations in the array. Now, instead of moving in a straight
line, the "square"
component will curve down to the right.
That's it! Next up, animating multiple components simultaneously.
This is where things get really interesting. Up until now, we've only been
dealing with a single "square"
component, but react-animatronics separates
itself from other libraries because it allows you to describe animations
involving multiple components anywhere in your component hierarchy:
import { Animatronics, Control } from 'react-animatronics'
const Square = ({ animatronicStyles }) => (
<div
style={{
backgroundColor: 'blue',
height: '100px',
width: '100px',
cursor: 'pointer',
position: 'absolute',
...animatronicStyles
}}
/>
);
const SquareOne = () => (
<Control name='squareOne'>{
({ animatronicStyles }) => (
<Square animatronicStyles={ animatronicStyles } />
)
}</Control>
);
const SquareTwo = () => (
<Control name='squareTwo'>{
({ animatronicStyles }) => (
<Square animatronicStyles={ animatronicStyles } />
)
}</Control>
);
const animations = [
{
squareOne: {
duration: 500,
from: { left: '0px' },
to: { left: '200px' }
},
squareTwo: {
duration: 500,
from: { top: '0px' },
to: { top: '200px' }
},
}
];
const App = () => (
<Animatronics animations={ animations }>{
({ playAnimation }) => (
<div onClick={() => playAnimation()}>
<SquareOne />
<SquareTwo />
</div>
)
}</Animatronics>
);
Take note of the animations
prop. It has a single phase, but within that phase
it describes animations for the two components "squareOne"
and "squareTwo"
.
When you execute the animation, you'll see one square move right and the other
move down at the same time.
Now is a good time to note that most of what you learn about animations
can
be combined. For example, you can combine simultaneous attribute animations
with simultaneous component animations:
const animations = [
{
squareOne: {
duration: 500,
from: {
left: '0px',
height: '100px',
},
to: {
left: '200px',
height: '200px'
}
},
squareTwo: {
duration: 500,
from: {
top: '0px',
width: '100px'
},
to: {
top: '200px',
width: '200px'
}
},
}
];
That's it! Next up, declaring different, named animations within the same
animations
prop.
You can declare multiple animations for a single <Animatronics>
component
by providing an animations
object instead of an array:
const animations = {
moveRight: [
{
square: {
duration: 500,
from: { left: '0px' },
to: { left: '200px' }
}
}
],
moveDown: [
{
square: {
duration: 500,
from: { top: '0px' },
to: { top: '200px' }
}
}
]
};
The object keys are the animation names and the values are the same animation arrays we've gone over in previous examples.
You can select which animation you want to execute by passing the string
name into playAnimation
:
playAnimation('moveRight');
playAnimation('moveDown');
Note, if you've declared named animations, you must pass a name into
playAnimation
. Otherwise, it will default to playing an animation
named "default"
, which won't exist unless you've defined that too.
You can still pass in a callback or call .then
on the returned promise:
playAnimation('moveRight', () => {
/* execute logic */
});
playAnimation('moveRight').then(() => {
/* execute logic */
});
To cancel your animations, pass the name into cancelAnimation
:
cancelAnimation('moveRight');
cancelAnimation('moveDown');
You can call cancelAnimation
with no arguments and it will cancel
all currently running animations.
However, resetting your animations is different. The reset
function never
accepts any arguments and will always reset the styles for every controlled
component under <Animatronics>
. There are two reasons for this. One, it keeps
the implementation simple, and two, I haven't run into a use case for resetting
named animations individually.
That's it! Next up, accessing DOM nodes in your animations.
You can access the DOM nodes of your controlled components inside your
animations
:
import { Animatronics, Control } from 'react-animatronics'
class Square extends React.Component {
render() {
const { animatronicsStyles, style } = this.props;
return (
<div
style={{
backgroundColor: 'blue',
height: '100px',
width: '100px',
cursor: 'pointer',
position: 'absolute',
...style,
...animatronicStyles
}}
/>
);
}
}
const SquareOne = () => (
<Control name='squareOne'>{
({ animatronicStyles }) => (
<Square
animatronicStyles={ animatronicStyles }
style={{
top: '0px',
left: '0px'
}}
/>
)
}</Control>
);
const SquareTwo = () => (
<Control name='squareTwo'>{
({ animatronicStyles }) => (
<Square
animatronicStyles={ animatronicStyles }
style={{
top: '0px',
left: '200px'
}}
/>
)
}</Control>
);
const SquareThree = () => (
<Control name='squareThree'>{
({ animatronicStyles }) => (
<Square
animatronicStyles={ animatronicStyles }
style={{
top: '200px',
left: '0px'
}}
/>
)
}</Control>
);
const animations = ({ squareOne, squareTwo, squareThree }) => {
const { left: leftOne, top: topOne } = squareOne.getBoundingClientRect();
const { left: leftTwo, top: topTwo } = squareTwo.getBoundingClientRect();
const { left: leftThree, top: topThree } = squareThree.getBoundingClientRect();
return [
{
squareOne: {
duration: 500,
from: {
left: `${ leftOne }px`,
top: `${ topOne }px`
},
to: {
left: `${ leftTwo }px`,
top: `${ topTwo }px`
}
},
squareTwo: {
duration: 500,
from: {
left: `${ leftTwo }px`,
top: `${ topTwo }px`
},
to: {
left: `${ leftThree }px`,
top: `${ topThree }px`
}
},
squareThree: {
duration: 500,
from: {
left: `${ leftThree }px`,
top: `${ topThree }px`
},
to: {
left: `${ leftOne }px`,
top: `${ topOne }px`
}
},
}
];
};
const App = () => (
<Animatronics animations={ animations }>{
({ playAnimation }) => (
<div onClick={() => playAnimation()}>
<SquareOne />
<SquareTwo />
<SquareThree />
</div>
)
}</Animatronics>
);
Note, animations
is now a function that returns the array we're already
familiar with. When animations
is a function, react-animatronics will call it
with an object. That object has keys that are the names of your controlled
components and values that are their DOM nodes. React-animatronics is able to
provide the DOM nodes because <Control>
attaches a ref to its child (make
sure the child function returns a class component, and not a stateless
component because React does not allow refs on stateless components).
In this example, we're animating three squares in a clockwise rotation. Each
square is moving to the position of another square, and we're getting the
actual positions of each square by calling getBoundingClientRect
on their DOM
nodes.
That's it! Next up, debugging your animations.
React-animatronics provides a helpful <DebugPanel>
component:
import { DebugPanel } from 'react-animatronics'
const App = () => (
<DebugPanel />
);
Simply mount the component anywhere in your app and you'll see this:
The <DebugPanel>
records all animations that go through react-animatronics.
You can replay the animation by clicking the "Replay" button. You can also
manually play back the animation by adjusting the slider or the "Current Frame"
input.
To hide the panel, you can click the blue "Hide Debug Panel" tab in the upper left.
To render the panel in a separate window, click on the "Window" button in the upper right.
It's up to you to determine when to render the <DebugPanel>
. You'll probably
want to put it behind a NODE_ENV === 'production'
check or just leave it out
entirely and only use it when needed.
That's it! We're going to be shifting gears now. The follwing sections will be quick overviews of the various options you can declare in your animations. Next up, springs.
Inspired by the awesome react-motion library, react-animatronics allows you to describe your animations with springs:
const animations = [
{
square: {
stiffness: 200,
damping: 20,
from: { left: '0px' },
to: { left: '200px' }
}
}
];
Instead of a millisecond duration
, you can provide a stiffness
and damping
.
If you haven't used spring animations, I highly recommend looking through the
react-motion library. It'll give you a good feel for how they work.
I adapted the spring implementation from react-motion into react-animatronics,
so the stiffness
and damping
values should function the same way.
That's it! Next up, a return to timed animations.
Whenever you provide your animations
with a duration
, react-animatronics
will provide a default easing function of cubic-bezier(0.4, 0.0, 0.2, 1)
,
which is the "Standard curve" from Material Design. Here's how
you would provide your own easing function:
import { BezierEasing } from 'react-animatronics'
const animations = [
{
square: {
duration: 500,
easing: BezierEasing(0.37, 0.11, 0.64, 1),
from: { left: '0px' },
to: { left: '200px' }
}
}
];
The BezierEasing
function comes straight from the bezier-easing library.
That's it! Next up, delaying your animations.
You can provide an optional, millisecond delay
to your animations
:
const animations = [
{
squareOne: {
duration: 500,
delay: 0,
from: { left: '0px' },
to: { left: '200px' }
},
squareTwo: {
duration: 500,
delay: 200,
from: { left: '0px' },
to: { left: '200px' }
},
squareThree: {
duration: 500,
delay: 400,
from: { left: '0px' },
to: { left: '200px' }
}
}
];
Doing so basically wraps the animation execution in a setTimeout
with the
given delay
. The primary use case for this is to create staggered animations.
That's it! We're almost done. The last section covers the range of values react-animatronics can interpolate.
Up until now, we've been using strings in the from
and to
attributes of our
animations, so we already know react-animatronics can correctly interpolate
strings such "0px"
to "100px"
. In order to do this, it does a simple
regex match to extract the number from the units. However, it can work with
more than just unit strings:
const animations = [
{
square: {
duration: 500,
from: {
left: '0px',
right: '0%',
opacity: 0,
backgroundColor: 'blue',
transform: 'scale(0)',
boxShadow: '0px 0px 0px red, 0px 0px 0px green'
},
to: {
left: '200px',
right: '20%',
opacity: 1,
backgroundColor: 'red',
transform: 'scale(1)',
boxShadow: '2px 2px 2px blue, 3px 3px 3px green'
}
}
}
];
It works with:
- numbers
- unit strings e.g.
"100px"
,"50rem"
- percentage strings
- colors
- transforms
- box shadows (
from
andto
must have the same number of shadows)
But that's not all! You can also animate between any valid CSS length:
const animations = [
{
square: {
duration: 500,
from: {
left: 'calc(100% - 5em)',
},
to: {
left: '0px',
}
}
}
];
However, be cautious when doing this. React-animatronics uses
getComputedStyle
to do this, so you may run into
unexpected behavior accross different browsers. It also needs to temporarily
manipulate the style
on the actual DOM nodes, so your controlled components
need to be class components.
One more thing! You can simultaneously animate different transforms:
const animations = [
{
square: [
{
duration: 500,
from: {
transform: 'scale(0)',
},
to: {
transform: 'scale(1)',
}
},
{
duration: 300,
from: {
transform: 'translateX(0px)',
},
to: {
transform: 'translateX(100px)',
}
},
{
duration: 400,
from: {
transform: 'translateY(0px)',
},
to: {
transform: 'translateY(100px)',
}
},
]
}
];
React-animatronics will merge the transformations so they all get applied as expected. This gives you a ton of flexibility with transforms, which is a good thing since they're less expensive to animate.
That's the end of the walkthrough. If any parts were confusing or unclear, please let me know by filing an issue or submitting a pull request.
Enjoy using react-animatronics and let me know how I can make it better. Thanks!