5 essential Motion patterns you should know
Animations are a core element of what makes a UI feel good. They help users understand where something came from and what just changed. Creating good animations is hard as there are many aspects to consider. Fortunately, tools like Motion (prev. Framer Motion) have made it easier to implement beautiful animations declaratively, which is why it’s one of the top choices for animation libraries for React developers. Here I’ll go over 5 Motion patterns that I’ve used extensively when building these animations.
First, let’s quickly go through the fundamentals by creating simple entrance animations with Motion. Most of your work with Motion is done through the <motion />
component.
import { motion } from "motion/react";
Now you can turn any HTML element into a Motion element by prepending a motion.
to the HTML tag.
// A simple div
<div>
Hi
</div>
// A ✨motion✨ div
<motion.div>
Hi
</motion.div>
A motion element is exactly the same as its static HTML element, except it has a few animation props like initial
and animate
. You can use the initial
prop to define the initial state of the div when it gets mounted , and the animate
prop to define the end state of the animation. Here’s a simple fade-in and scale animation:
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
/>
1. Exit animations
To animate an element as it is exiting the DOM, you can use the exit
prop. The most important thing to remember with exit animations is that you have to wrap the exiting element with <AnimatePresence />
. Let’s add an exit animation to the ball, which gets triggered on click.
When you wrap an element with AnimatePresence
, Motion will be able to detect exactly when the element is removed from the React tree, and will then animate it based on the styles defined in the exit
prop.
In addition to using conditional rendering with logical AND (&&
), Motion provides another powerful way to handle entrance and exit animations: using the key
prop.
Changing the key
prop tells React that it's dealing with a different component entirely, even if it looks the same, which allows Motion to detect the old element exiting the DOM and the new one entering.
const [currentBall, setCurrentBall] = useState("lime");
const handleToggleBall = () => {
setCurrentBall((currentBall) => (currentBall === "lime" ? "teal" : "lime"));
};
return (
<AnimatePresence mode="wait">
<motion.div
key={currentBall} //Don't forget the key prop!
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 100 }}
className={`${currentBall === "lime" ? "bg-lime-500" : "bg-teal-500"}`}
onClick={handleToggleBall}
/>
</AnimatePresence>
);
Notice the mode="wait"
on the AnimatePresence. The default value of mode
is sync
which animates the elements in and out as soon as they’re added/removed. Here we’re using wait
which first animates the existing element out, and only then animates the new element in. You can read more about the mode
prop here.
2. Staggered animations
Sometimes we want to animate a list of elements one after the other, also known as a “staggered animation”. An imperative way of implementing this is to simply add a delay to each element, often a function of the index of the item.
We can achieve this by using the transition
prop. We can pass an object to transition
that tells Motion the type of animation we want with things like duration
, delay
and ease
. Here’s a staggered balls animation:
Here's an example of staggered animations using the transition
prop with Motion:
<div className="flex gap-4">
{[0, 1, 2, 3, 4].map((index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay: 0.1 * index, // Each ball is delayed by 0.1s * index
}}
/>
))}
</div>
The key part is the delay: 0.1 * index
in the transition prop, which creates that nice sequential animation effect as each ball appears slightly after the previous one.
A more declarative way of doing staggered animations is using variants. Variants are a nicer way of defining the animation states using a key - value pair. Let’s recreate our entrance animation from earlier using variants:
const variants = {
hidden: { opacity: 0, scale: 0 },
visible: { opacity: 1, scale: 1 },
}
...
<motion.div
variants={variants}
initial="hidden"
animate="visible"
/>
Here we defined our different animation states in a variants
object, which we then passed to the motion component via the variants
prop. This allows us to now use keys instead of objects for our initial
and animate
props.
Going back to our staggered animation, here’s how we’d implement it via variants
:
const containerVariants = {
visible: {
transition: {
staggerChildren: 0.1, // This creates the stagger effect
},
},
};
const ballVariants = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0 },
};
return (
// The parent container controls the staggering
<motion.div
className="flex gap-4"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{[0, 1, 2, 3, 4].map((index) => (
<motion.div
key={index}
variants={ballVariants}
// No need to specify initial and animate here!
// They are automatically inherited from the parent
/>
))}
</motion.div>
);
With variants, we can create a much cleaner implementation of staggered animations. Notice how we don't need to specify the initial
and animate
props on the child elements - they automatically inherit these values from the parent. The staggerChildren
property in the parent's transition object controls the delay between each child's animation.
I didn’t really want to animate the container there because it was invisible, but if you wanted to you can just change the containerVariants
to include the entrance and exit animations.
const containerVariants = {
hidden: { opacity: 0, y: 10 },
visible: {
opacity: 1,
y: 0,
transition: {
staggerChildren: 0.1,
when: "beforeChildren",
},
},
};
const ballVariants = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0 },
};
Notice the when: "beforeChildren"
, which tells Motion to animate the container first and then the staggered balls animation.
3. Animating height: "auto"
This is one of my favourite uses for Motion. CSS by itself can’t animate from height: 0
to height: "auto"
which is a very common animation, especially for things like accordions. Motion, however, allows us to animate from a numerical value (e.g. 0
) to "auto"
and vice-versa (from "auto"
to 0
). Let’s see this in action with a simple FAQ accordion component.
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
<span>Click me to expand!</span>
<ChevronDown
className={`transition-transform ${isOpen ? "rotate-180" : ""}`}
/>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="overflow-hidden"
>
...
</motion.div>
)}
</AnimatePresence>
</div>
);
Another very common animation is when we animate from height: "auto"
to height: "auto"
. In this case, we’re not animating from/to a fixed value so we have to literally measure the height. I like to use react-use-measure
.
import useMeasure from "react-use-measure";
The useMeasure
hook returns an array with a ref
and a bounds
object. We just have to pass the ref to the element whose height we’re measuring, and useMeasure
takes care of the rest.
const [ref, bounds] = useMeasure();
return (
<div ref={ref}>
Drag to resize!
{`${bounds.width} x ${bounds.width} px`}
</div>
);
Now we can set the measured height in the animate
prop so Motion can animate to the correct height when the size of the content changes.
let [ref, bounds] = useMeasure();
return (
<motion.div
animate={{
height: bounds.height,
}}
>
<div ref={ref}>...</div>
</motion.div>
);
One small bug fix. You may notice a slight jitter on the initial render. This is because useMeasure
returns 0 for the bounds
values on the first render, so simply checking for bounds.height
being positive fixes this.
<motion.div animate={{ height: bounds.height > 0 ? bounds.height : null }} />
4. Direction aware animations
This one’s a short one but very important. We’ll start with a simple carousel component.
const slides = [1, 2, 3, 4, 5];
const [index, setIndex] = useState(0);
const prev = () => setIndex((i) => (i === 0 ? slides.length - 1 : i - 1));
const next = () => setIndex((i) => (i === slides.length - 1 ? 0 : i + 1));
return (
<div className="flex items-center justify-center gap-4">
<button onClick={prev}>
<ChevronLeft />
</button>
<div>{slides[index]}</div>
<button onClick={next}>
<ChevronRight />
</button>
</div>
);
Now we’ll start animating our carousel. This part will be a nice review of the concepts covered up to this point. To animate the center slide, we can turn this into a motion.div
and set the x
position to the correct stage of the animation
<motion.div initial={{ x: "100%" }} animate={{ x: "0%" }} exit={{ x: "-100%" }}>
{slides[index]}
</motion.div>
Now just like before, we need to wrap this in an AnimatePresence
so the exit animation works. We need to also use a key
to tell React when to mount and unmount our slide. I added initial={false}
to AnimatePresence
so we don’t see an entrance animation on the first render, and only when we change slides.
<AnimatePresence initial={false}>
<motion.div
key={`slide-${index}`}
initial={{ x: "100%" }}
animate={{ x: "0%" }}
exit={{ x: "-100%" }}
>
{slides[index]}
</motion.div>
</AnimatePresence>
We see our carousel kinda working, but the animations are not direction aware. The slides are always entering from the right and exiting from the left regardless of which button we press.
To fix this, let’s first move our animation states into a variants object.
const variants = {
initial: { x: "100%" },
animate: { x: "0%" },
exit: { x: "-100%" }
}
...
<AnimatePresence initial={false}>
<motion.div
key={`slide-${index}`}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
>
{slides[index]}
</motion.div>
</AnimatePresence>
Now we need a way to keep track of the direction. We’ll store this in a React state
import { useState } from "react";
const slides = [1, 2, 3, 4, 5];
const variants = {
initial: { x: "100%" },
animate: { x: "0%" },
exit: { x: "-100%" },
};
const [index, setIndex] = useState(0);
const [direction, setDirection] = useState<1 | -1 | undefined>(undefined);
const prev = () => {
setDirection(-1);
setIndex((i) => (i === 0 ? slides.length - 1 : i - 1));
};
const next = () => {
setDirection(1);
setIndex((i) => (i === slides.length - 1 ? 0 : i + 1));
};
return (
<div className="flex items-center justify-center gap-4">
<button onClick={prev}>
<ChevronLeft />
</button>
<AnimatePresence initial={false}>
<motion.div
key={`slide-${index}`}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
>
{slides[index]}
</motion.div>
</AnimatePresence>
<button onClick={next}>
<ChevronRight />
</button>
</div>
);
Now we need a way to pass the direction to our variants, so that our variants become dynamic. We can do this via the custom
prop. We’ll pass the direction to the custom prop, and redefine our variants to use the direction as a parameter.
let variants = {
initial: (direction) => {
return { x: `${100 * direction}%` };
},
animate: { x: "0%" },
exit: (direction) => {
return { x: `${-100 * direction}%` };
},
};
...
<AnimatePresence initial={false}>
<motion.div
key={`slide-${index}`}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
custom={direction}
>
{slides[index]}
</motion.div>
</AnimatePresence>
This almost works. Notice that the slides are entering from the correct direction, but the exit animations are using a stale direction
value. This is because of how React works. From Motion’s docs:
When a component is removed, there's no longer a chance to update its props (because it's no longer in the React tree). Therefore we can't update its exit animation with the same render that removed the component.
Fortunately, Motion gives us a simple solution. We just have to pass the custom prop to AnimatePresence
as well, and the exit animation will have access to the latest direction
value.
<AnimatePresence initial={false} custom={direction}>
...
</AnimatePresence>
5. Layout animations
This is perhaps the most powerful feature of Motion. With layout animations, you’re able to create a smooth transition from an old position/size to a new one. You simply define the start and the end of the layout, and Motion will figure out the middle states.
Here we’ll animate a simple toggle component.
const [on, setOn] = useState(false);
<div
style={{
display: "flex",
justifyContent: isOn ? "flex-start" : "flex-end",
}}
>
<span className="rounded-full" />
</div>;
Now all we have to do to animate this is turn the div
into a motion.div
and add a layout
prop, and Motion will take care of the rest.
const [on, setOn] = useState(false);
<motion.div
layout
style={{
display: "flex",
justifyContent: isOn ? "flex-start" : "flex-end",
}}
>
<span className="rounded-full" />
</motion.div>;
With just the addition of one prop we’re able to animate our toggle. As you can tell, there’s a lot of “magic” going on behind the scenes which is why sometimes layout animations can be hard to debug.
Motion also provides an API for “shared” layout animations. This allows you to create a transition between two different elements that share the same layoutId
. A simple example is the following navigation menu animation.
const items = ["Home", "About", "Contact", "Services", "Products"];
const [activeItem, setActiveItem] = useState<string | undefined>("Home");
return (
<div className="flex items-center justify-center gap-4">
{items.map((item) => (
<button
key={item}
onMouseOver={() => setActiveItem(item)}
className="relative"
>
{activeItem === item && (
<motion.span
layoutId="bubble"
className="absolute inset-0 bg-black/10rounded-md"
/>
)}
<span className="relative z-10">{item}</span>
</button>
))}
</div>
);
Notice how every element is conditionally rendering its own highlighted span, but because of the layoutId
, Motion is able to create a transition between its different positions.
Key Takeaways
- Use the
exit
prop andAnimatePresence
to implement exit animations. - Use the
key
prop to tell React when to mount/unmount an animation. - To implement staggered animation, you can either incrementally increase the
delay
in thetransition
prop for each node, or you can simply passstaggerChildren
to the parent container. - Motion can easily animate between a fixed height to
"auto"
and vice versa. If you want to animate between dynamic heights, use theuseMeasure
hook. - For direction aware animations, pass the direction to the
custom
prop of your motion component and then use it as a parameter in yourvariants
. Make sure to also include thecustom
prop inAnimatePresence
if you have exit animations. - Add the
layout
prop to animate size and positioning. If you want to animate between two different elements, you can use shared layout animations vialayoutId
.
That’s it! I hope these patterns will help you with your animations. If you make something cool, feel free to share it with me. Happy animating!