Issue 4May 2026
How I use shared layout animations
Shared layout animations in Motion (opens in a new tab) are very powerful. They let you smoothly animate between different elements across different states with a single line of code.
Click between the tabs to see the shared layout animation in action
Single element
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 but we’ll 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 (opens in a new tab) technique, which stands for First, Last, Invert, Play.
If you’re interested in how it works, I recommend reading Inside Framer’s Magic Motion (opens in a new tab) by Nanda (opens in a new tab), a beautiful article that explains the concept really well.
Multiple elements
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 that tells AnimatePresence to run the exit animation when the element unmounts and the initial animation when it mounts.
Changing values between states
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.
Other examples
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 (opens in a new tab) documentation.
Hover over the card and click the fullscreen button in the top left
I used shared layout animations paired with the Base UI Dialog primitive (opens in a new tab). The idea is the same as in the other examples. There are two separate components: a card and a dialog. Motion animates between them using a shared layoutId.
Start building great interfaces today
Quality matters more than it ever has in software. Just building something isn't enough. If you care about craft, like to sweat the details and want to learn more about design engineering, I think you’ll enjoy the magazine.
Join the other 1,458 readers today!
New issue every month
Resource library
Interactive demos
Source code
Agent skills
Private Discord community
New issue every month
Resource library
Interactive demos
Source code
Agent skills
Private Discord community
Everything in Monthly and Yearly, plus:
Early access to new issues
Lifetime member badge
Vote on upcoming topics