Search code examples
reactjsframer-motion

Framer-Motion - How to animate multiple cards


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.

Framer-Motion card animation

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).

Rendered version using given code snip

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>
  )
}

Solution

  • 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;