For a few years, micro-animations became more and more popular in web apps, and they are not only reserved for native mobile apps.
Using them is a way to drive the user during his navigation or to give some depth to the content, and that's why they will transform a user experience that simply "does the job" into an experience "that makes you wow"
I have tried to develop native micro-animations, with CSS and VanillaJS, in the past to make my React projects shinier. However, I've always been blocked by some recurrent problems :
- enter/leave animations are tricky to setup
- a lot of code is duplicated from a component to another
- orchestrating animations between several components is a nightmare
The result I got was not fluid, and it didn't worth the time I spent developing it.
Lately, I discovered Framer Motion, a React library to create smooth animations, and even if I didn't get the occasion to test all of its options, it already got me out of tricky situations.
👉 In this article, I'll illustrate my words with the Card component bellow and a complete preview can be found on CodeSandbox
import { FC } from "react";
import styles from "./Card.module.scss";
interface CardProps {
title: string;
desc: string;
category: string;
price: string;
image: string;
}
const Card: FC<CardProps> = ({ image, title, category, price, desc }) => {
return (
<div
className={styles.card}
>
<img src={image} className={styles.image} alt="" />
<div className={styles.content}>
<h2 className={styles.title}>{title}</h2>
<div className={styles.subtitle}>
{price} • {category}
</div>
<div className={styles.desc}>{desc}</div>
</div>
</div>
);
};
export default Card;
Animate an HTML element
Let's add framer-motion
to your project.
yarn add framer-motion
Then import Motion into your component.
import { motion } from 'framer-motion'
This helper will provide you access to lots of Motion components (ie: motion.div, motion.span, motion.path...) : there's one for every HTML and SVG element. They can be used as standard native elements, and they also accept props that are specific to Motion.
initial
et animate
are props to define the initial state of the component and the values they will be animated to once it will be mounted into the DOM.
By default, Motion will provide some default transitions depending on the props you animate, but you can obviously define your own transitions.
<motion.div
className={styles.card}
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", bounce: 0.5 }}
>
{/* ... */}
</motion.div>
In order to make the animation reusable, Motion accepts a variant object that describes several animations. initial
and animate
will then need the key of the variant to use.
const bounceVariants = {
hidden: { opacity: 0, scale: 0.6 },
visible: {
opacity: 1,
scale: 1,
transition: {
type: "spring",
bounce: 0.5
}
}
};
<motion.div
className={styles.card}
initial="hidden"
animate="visible"
variants={bounceVariants}
>
{/* ... */}
</motion.div>
Overload a React component to animate it
We discovered how to animate an HTML element, which is very useful when you are able to edit your own code. However, React is by design made to manipulate reusable components. How to animate a component when you want to animate that is not part of the project, if it belongs to an external component library (MaterialUI, Ant.design...) or it lives into your organization's design system?
In this configuration, motion()
helper will be a useful function to encapsulate your component into an animable one. Like other motion
components, this new component will behave just like you've been used to, and it is inherited to all the custom Motion props.
This component overload should be done outside of the render function of the parent component.
import { Button } from '@corp-designsystem/react'
const AnimatedButton = mount(Button)
const Card = () => {
return <div className={styles.button}>
<AnimatedButton
initial="hidden"
animate="visible"
variants={fadeUpVariants}
>
Book
</AnimatedButton>
</div>
}
Motion animation is played when the component is mounted. If you couldn't see this one, please refresh the page
💡 This method will only work for components that are passing their ref to their root component using
React.forwardRef
. If it's not your case, a trade-off can be to wrap the component into amotion.div
container that you will animate.
Cascade animations from a component to its children
What we've done yet only need a couple of lines if you're familiar with CSS3 keyframes.
However, to coordinate animations between a component and its children, you'll need to calculate every delay between all your component's animations. A change a Motion component variant will be spread to its children variants on conditions that their key identifiers are the same. That's to the transition
prop, it is then possible to define extra properties to orchestrate animations between the children.
- when: define if a component should be animated before its children (
beforeChildren
) or after (afterChildren
). - childrenDelay : the delay between a parent's animation and its children's
- staggerChildren: delay between every child
- staggerDirection: if staggerDirection equals 1, children will be animated from the first to the last in the DOM order, and the opposite if it equals -1.
Refresh the page if you couldn't see the animation.
Control animations depending on external effects
For now, the Card animations only get executed when the component is mounted on the DOM. If it is below the waterline, animations won't be seen by the user. That's why I used the Intersection Observer API and a Motion hook called useAnimation
to control it.
yarn add react-intersection-observer
Import useInView
hook from react-intersection-observer and useAnimation
from framer-motion.
useInView
is a small hook that is used to know if a given component is visible or not.
useAnimation
returns a controls
object to programmatically manipulate variant animations.
I finally used useEffect to alternate betwwen the variant states, visible and hidden,
const Card: FC<CardProps> = ({ image, title, category, price, desc }) => {
const { ref, inView } = useInView();
const controls = useAnimation();
useEffect(() => {
if (inView) {
controls.start("visible");
}
if (!inView) {
controls.start("hidden");
}
}, [inView, controls]);
return (
<motion.div
className={styles.card}
initial="hidden"
variants={bounceVariants}
animate={controls}
ref={ref}
></motion.div>)
}
You can replay this animation when scrolling, to show/hide the card
Conclusion
Motion has been a great help to integrate some orchestrated animations easily, without the need to compute intervals and delays between every single element.
This library has a lot of other features that I didn't tried yet: animate on component unmount, some helpers to animate on tap, focus or click, animate SVG... Motion will make my micro-animations more fluid, and I can now consider animating component to make my React apps alive, even if my client's budget is tight.
🛼 To continue my experiments with Framer Motion, I added some animations to this website. Did you notice them?