TL;DR - How to animate multiple cards with react and framer-motion?
Hi,
I doubt I am the first one to ask this (but I can't seem to find a solid answer anywhere), but I am struggling to recreate the Card Animation from the Framer-Motion.
I am looking to recreate the click-expand and focus effect I see on the page, but I can only seems to get it to work for 1 card. The below code only renders the cards but I can't seem to give them animation and I am definitely stuck on what to do next (i.e. I've definitely missed something on how to control the card state with react).
const JobsPage = () => {
const [selectedId, setSelectedId] = useState(null)
const cards = [{
id: 1,
subtitle: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. A, minima?',
title: 'Card 1'
}, {
id: 2,
subtitle: 'Lorem ipsum dolor sit amet.',
title: 'Card 2'
}, {
id: 3,
subtitle: 'Lorem ipsum dolor sit amet.',
title: 'Card 3'
}, {
id: 4,
subtitle: 'Lorem ipsum dolor sit amet.',
title: 'Card 4'
}]
return (
<div class="">
<h2 class="text-2xl font-bold flex justify-center">
Jobs
</h2>
{/* Jobs Tab */}
<div class="grid grid-cols-4 bg-green-300 justify-between p-4 space-x-2">
{cards.map(card => (
<motion.div layoutId={card.id} class="bg-white p-2" onClick={() => setSelectedId(card.id)}>
<motion.h2>{card.title}</motion.h2>
<motion.h5>{card.subtitle}</motion.h5>
</motion.div>
))}
<AnimatePresence>
{selectedId && (
<motion.div layoutId={selectedId}>
{/* <motion.h5>{cards.subtitle}</motion.h5>
<motion.h2>{cards.title}</motion.h2> */}
<motion.button onClick={() => setSelectedId(null)} />
</motion.div>
)}
</AnimatePresence>
</div>
</div>
)
}
Found this article that recreated a similar (but with more features) version of what you're looking for:
https://framermotionplayground.com/tutorial/layout-cards
Only posting this as an answer because I can't comment lol. Hope this helps.
Edit:
Decided to create a Code Sandbox and give a working solution to this problem. Code Sandbox is here: https://codesandbox.io/p/sandbox/ecstatic-sid-c975nw
The relevant code/component is as follows:
import { motion, AnimatePresence } from 'framer-motion';
const App = () => {
const [selectedId, setSelectedId] = useState('');
const items = [
{
id: '1',
title: 'Card 1',
subtitle: 'Information 1',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
},
{
id: '2',
title: 'Card 2',
subtitle: 'Information 2',
description: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
},
{
id: '3',
title: 'Card 3',
subtitle: 'Information 3',
description:
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.',
},
{
id: '4',
title: 'Card 4',
subtitle: 'Information 4',
description:
'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
},
];
return (
<motion.div className="bg-purple-600 flex items-center justify-center h-screen">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{items.map((item) => (
<motion.div
className={`card bg-white rounded-lg shadow-md cursor-pointer transform transition-transform duration-500 hover:scale-105 ${
selectedId === item.id ? 'card-selected' : ''
}`}
layoutId={`card-container-${item.id}`}
onClick={() => setSelectedId(item.id)}
key={item.id}
initial={{ scale: 1 }}
animate={{ scale: selectedId === item.id ? 1.1 : 1 }} // Increase scale on selected card
transition={{ duration: 0.3 }}
>
<div className="card-content">
<motion.h2 className="text-xl font-bold mb-2 text-purple-600">{item.title}</motion.h2>
<motion.h5 className="text-sm font-bold mb-1 text-gray-700">{item.subtitle}</motion.h5>
</div>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && (
<motion.div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{items.map((item) => (
item.id === selectedId && (
<motion.div
className="bg-white rounded-lg p-4 shadow-md max-w-lg mx-auto"
layoutId={`card-container-${item.id}`}
key={item.id}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
>
<motion.div className="relative">
<motion.button
className="absolute top-2 right-2 py-1 px-2 text-center text-white bg-red-500 rounded-full"
onClick={() => setSelectedId('')}
>
Close
</motion.button>
<motion.h2 className="text-xl font-bold mb-2 text-purple-600">{item.title}</motion.h2>
<motion.h5 className="text-sm font-bold mb-1 text-gray-700">{item.subtitle}</motion.h5>
<motion.p className="text-md text-gray-700 mb-4">{item.description}</motion.p>
<motion.p
className="text-md text-gray-700"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Additional content can go here!
</motion.p>
</motion.div>
</motion.div>
)
))}
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
export default App;