How I use shared layout animations
Shared layout animations in Motion are very powerful. They let you smoothly animate between different elements across different states with a single line of code.
Click between different tabs to see the animations in action
To use shared layout animations, first turn the element you want to animate into a motion component and add the layoutId prop.
In this example, a switch statement renders the same circle in different positions based on the selected tab.
const getTabContent = () => {
switch (activeTab) {
case "Design":
return (
<motion.div
className="size-16 rounded-full bg-gray-1200"
layoutId="circle"
/>
);
case "Engineering":
return (
<motion.div
className="absolute top-4 right-4 size-16 rounded-full bg-gray-1200"
layoutId="circle"
/>
);
case "Product":
return (
<motion.div
className="absolute top-4 left-4 size-16 rounded-full bg-gray-1200"
layoutId="circle"
/>
);
case "Marketing":
return (
<motion.div
className="absolute bottom-4 left-4 size-16 rounded-full bg-gray-1200"
layoutId="circle"
/>
);
case "Sales":
return (
<motion.div
className="absolute right-4 bottom-4 size-16 rounded-full bg-gray-1200"
layoutId="circle"
/>
);
default:
return null;
}
};You can see that we don't manipulate an individual component. Instead, we render different components in different places in different states that happen to look the same. Their appearance can also be different though, we will cover that later.
All that is necessary for these elements to animate is that they share the same layoutId and Motion does all the heavy lifting to animate between them.
<motion.div
className="size-16 rounded-full bg-gray-1200"
layoutId="circle"
/>The way this works under the hood is by using the FLIP technique, which stands for First, Last, Inverse, Play.
If you're interested in how it works, I recommend reading Inside Framer's Magic Motion by Nanda, a beautiful article that explains the concept really well.
You can animate between multiple elements too.
As long as the elements share the same layoutId and that Id is unique within the group, the animation will work correctly.
<div className="relative flex h-full w-full items-center justify-center gap-8">
<motion.div
className="size-16 rounded-full bg-orange-500"
layoutId="circle-1"
/>
<motion.div
className="size-16 rounded-full bg-blue-500"
layoutId="circle-2"
/>
<motion.div
className="size-16 rounded-full bg-green-500"
layoutId="circle-3"
/>
</div>If you assign the same layoutId to multiple elements in the same state, the animation will break, like in the example below.
Now the only remaining thing is to animate the arrows between the circles to match the animation from the beginning of the article.
The arrows are wrapped in AnimatePresence so they can fade in and out as the layout changes. The circles stay outside of AnimatePresence, since we don't want them to fade in and out.
<AnimatedCircle layoutId="circle-1" />
<AnimatePresence>
<motion.div
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(4px)" }}
initial={{ opacity: 0, filter: "blur(4px)" }}
key="divider-1"
>
<ArrowMarquee arrowCount={8} />
</motion.div>
</AnimatePresence>
<AnimatedCircle layoutId="circle-2" />Each arrow wrapper has a key which tells AnimatePresence to run the exit animation when the element unmounts and the initial animation when it mounts.
The elements you animate between can have different property values like height, width, position and more.
Click between different tabs to see the animations in action
In this example, the elements have different positions, sizes and border radii. The border radius is passed to animate instead of a className so Motion can smoothly interpolate between the values.
const getCardConfig = () => {
switch (activeTab) {
case "Design":
return {
className: "top-24 left-46 h-12 w-32",
borderRadius: "8px",
};
case "Engineering":
return { className: "top-10 right-2 h-78 w-32", borderRadius: "8px" };
case "Product":
return { className: "bottom-2 left-2 h-20 w-28", borderRadius: "8px" };
case "Marketing":
return { className: "top-17 left-49 size-16", borderRadius: "4px" };
case "Sales":
return { className: "bottom-2 right-2 size-8", borderRadius: "32px" };
}
};
<motion.div
className={cn("absolute bg-gray-1200", cardConfig.className)}
animate={{ borderRadius: cardConfig.borderRadius }}
layoutId="card"
/>AnimatePresence is also used to animate the content behind the moving element.
When combining AnimatePresence and shared layout animations, keep components with layoutId outside of AnimatePresence if you don't want the initial and exit animations to affect them during the layout transition.
If they are inside, the initial and exit animations will trigger while the elements are animating. This doesn't look great, especially if you are animating properties like opacity.
Notice the container fading in and out as the layout changes, this is not what we want.
There are many other creative examples of shared layout animations. One of my personal favorites is a preview component like the one below. There are also other great examples in the Motion documentation.
Hover over the card and click the fullscreen button in the top left
I used shared layout animations paired with the Radix Dialog primitive. The idea is the same as in the other examples. There are two separate components: a card and a dialog and Motion animates between them using a shared layoutId.
More
If you have any questions you can reach me via email, see more of my work on X (Twitter) or subscribe to my newsletter below.