Shared Layout Animations
Shared layout animations in Motion are very powerful. They allow you to animate between different elements with a single line of code.
Single Element
To use shared layout animations, we first need to turn the element that we want to animate into a motion component and add the layoutId prop.
It uses the FLIP technique to animate elements, which stands for First, Last, Inverse, Play. If you're interested in how it works under the hood, I recommend reading Inside Framer's Magic Motion by Nanda.
<motion.div
className="size-16 rounded-full bg-gray-1200"
layoutId="circle"
/>
In the example above, 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;
}
};
We're not manipulating the component itself. Instead, we render different components for each state with the same layoutId and Motion does all the heavy lifting to animate between them.
Multiple Elements
You can animate as many elements as you want.
As long as the elements that you want to transition between have the same layoutId and that id is unique to 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>
Other Components
The elements you animate between can have different heights, widths, positions, etc.
In this component I'm also using AnimatePresence to animate the exit of the content. When using both AnimatePresence and shared layout animations, I recommend keeping components with layoutId outside of AnimatePresence.
<div className="relative flex h-[320px] w-full items-start justify-center">
<motion.div
className={cn("card", cardConfig.className)}
layoutId="card"
style={{ borderRadius: cardConfig.borderRadius }}
transition={{
type: "spring",
duration: 0.55,
bounce: 0.1,
}}
/>
<div className="flex w-full flex-col items-start justify-center">
<Navbar />
<AnimatePresence>
<motion.div
animate={{
opacity: 1,
filter: "blur(0px)",
scale: 1,
translateY: 0,
}}
exit={{
opacity: 0,
filter: "blur(4px)",
scale: 0.95,
translateY: 12,
}}
initial={{
opacity: 0,
filter: "blur(4px)",
scale: 0.95,
translateY: 12,
}}
key={activeTab}
transition={{
type: "spring",
duration: 0.55,
bounce: 0,
}}
>
{getContent()}
</motion.div>
</AnimatePresence>
</div>
</div>
If they are inside, the initial and exit animations will trigger while the elements are animating, which doesn't look great especially if you are animating properties like opacity.
For this next component I used shared layout animations paired with the Radix Dialog primitive. The idea is exactly the same, where there are two separate components: the card and the dialog and Motion animates between them using a shared layoutId.
More
In case you have any questions reach me at jakub@kbo.sk, see more of my work on Twitter or subscribe to my newsletter.
Newsletter
I share stuff that I'm working on, new posts and resources here.