I am trying to create a dynamic layout with framer motion in Next.js.
As you can see in the GIF below, AnimatePresence does not respect layout changes but rather fades out the elements in the same position.
When changing the “mode” in AnimatePresence, these things happen:
- “popLayout” and “sync”: Behavior unchanged
- “wait”: The text fades first, the layout animation plays after that
The internet, ChatGPT and Copilot all don’t know how to fix this – so I’d appreciate any help 🙂
I already thought of adding the title/content to the disabled state with opacity=0 and width=0, but I don’t think that’s the way to go – especially if I want my layout to become more complex.
This is my code:
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
const items: { [key: string]: any } = {
["timeline-placeholder1"]: {
icon: (
// Academic Cap
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" />
</svg>
),
title: "Placeholder Title 1",
content: "Placeholder Content 1",
},
["timeline-placeholder2"]: {
icon: (
// Office Building 2
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Z" />
</svg>
),
title: "Placeholder Title 2",
content: "Placeholder Content 2",
},
};
export default function TimelineWidgetContent() {
const [selected, setSelected] = useState<string | null>(null);
const handleSelect = (key: string) => {
setSelected(key);
};
const handleDeselect = () => {
setSelected(null);
};
const transition = {
duration: 2,
ease: "easeInOut",
};
return (
<div className="h-screen w-screen flex justify-center items-center">
<div className="w-[32rem] h-48 bg-slate-700">
<div className="relative w-full h-full p-4 flex flex-col items-center">
<h1 className="text-4xl font-bold self-center">Timeline</h1>
<AnimatePresence>
{selected ? (
<motion.div
key={selected}
layoutId={selected}
onClick={() => handleDeselect()}
className="absolute z-10 top-0 left-0 w-full h-full bg-blue-500 text-secondary-content px-4 overflow-hidden"
transition={transition}
layout
>
<div className="flex gap-6 items-center">
<motion.div
key={`${selected}-icon`}
layoutId={`${selected}-icon`}
className="w-16 h-16 flex justify-center items-center"
transition={transition}
layout="position"
>
{items[selected].icon}
</motion.div>
<motion.div
key={`${selected}-title`}
layoutId={`${selected}-title`}
transition={transition}
layout="position"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<h2 className="text-2xl font-bold">{items[selected].title}</h2>
</motion.div>
</div>
<motion.div
key={`${selected}-content`}
layoutId={`${selected}-content`}
transition={transition}
layout="position"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<p>{items[selected].content}</p>
</motion.div>
</motion.div>
) : null}
<div className="flex h-full items-center">
{Object.entries(items).map(([key, item]) =>
key === selected ? (
<div className="flex items-center" key={key}>
<div className="w-16 border-2 border-blue-500" />
<div className="w-20 h-20" />
</div>
) : (
<div className="flex items-center" key={key}>
<div className="w-16 border-2 border-blue-500" />
<motion.div
key={key}
layoutId={key}
onClick={() => handleSelect(key)}
className="w-20 h-20 bg-blue-500 flex justify-center items-center select-none cursor-pointer"
style={{
borderRadius: "50%",
}}
transition={transition}
>
<motion.div
key={`${key}-icon`}
layoutId={`${key}-icon`}
className="w-16 h-16 flex justify-center items-center"
transition={transition}
layout="position"
>
{item.icon}
</motion.div>
</motion.div>
</div>
)
)}
<div className="w-16 border-2 border-blue-500" />
{/* Arrow Tip */}
<div className="w-0 h-0 border-t-[8px] border-t-transparent border-b-[8px] border-b-transparent border-l-[16px] border-l-blue-500" />
</div>
</AnimatePresence>
</div>
</div>
</div>
);
}