Search code examples
reactjscarouselslideshowframer-motion

How to do a 3D carousel wth React


I cannot find how to do a 3D carousel (aka slideshow) with React being able to show at least three elements. There seems not to be up-to-date libraries or components for that in npm :(

Here is what it should look like:

enter image description here


Solution

  • After experimenting for a while, here is how I manage to do it using framer motion:

    import './styles.css';
    import { AnimatePresence, motion } from 'framer-motion';
    import { useState } from 'react';
    
    export default function App() {
      const [[activeIndex, direction], setActiveIndex] = useState([0, 0]);
      const items = ['🍔', '🍕', '🌭', '🍗'];
      
      // we want the scope to be always to be in the scope of the array so that the carousel is endless
      const indexInArrayScope =
        ((activeIndex % items.length) + items.length) % items.length;
      
      // so that the carousel is endless, we need to repeat the items twice
      // then, we slice the the array so that we only have 3 items visible at the same time
      const visibleItems = [...items, ...items].slice(
        indexInArrayScope,
        indexInArrayScope + 3
      );
      const handleClick = newDirection => {
        setActiveIndex(prevIndex => [prevIndex[0] + newDirection, newDirection]);
      };
    
      return (
        <div className="main-wrapper">
          <div className="wrapper">
            {/*AnimatePresence is necessary to show the items after they are deleted because only max. 3 are shown*/}
            <AnimatePresence mode="popLayout" initial={false}>
              {visibleItems.map((item) => {
                // The layout prop makes the elements change its position as soon as a new one is added
                // The key tells framer-motion that the elements changed its position
                return (
                  <motion.div
                    className="card"
                    key={item}
                    layout
                    custom={{
                      direction,
                      position: () => {
                        if (item === visibleItems[0]) {
                          return 'left';
                        } else if (item === visibleItems[1]) {
                          return 'center';
                        } else {
                          return 'right';
                        }
                      },
                    }}
                    variants={variants}
                    initial="enter"
                    animate="center"
                    exit="exit"
                    transition={{ duration: 1 }}
                  >
                    {item}
                  </motion.div>
                );
              })}
            </AnimatePresence>
          </div>
          <div className="buttons">
            <motion.button
              whileTap={{ scale: 0.8 }}
              onClick={() => handleClick(-1)}
            >
              ◀︎
            </motion.button>
            <motion.button whileTap={{ scale: 0.8 }} onClick={() => handleClick(1)}>
              ▶︎
            </motion.button>
          </div>
        </div>
      );
    }
    
    const variants = {
      enter: ({ direction }) => {
        return { scale: 0.2, x: direction < 1 ? 50 : -50, opacity: 0 };
      },
      center: ({ position }) => {
        return {
          scale: position() === 'center' ? 1 : 0.7,
          x: 0,
          zIndex: zIndex[position()],
          opacity: 1,
        };
      },
      exit: ({ direction }) => {
        return { scale: 0.2, x: direction < 1 ? -50 : 50, opacity: 0 };
      },
    };
    
    const zIndex = {
      left: 1,
      center: 2,
      right: 1,
    };
    
    

    Here is a code sandbox with the solution: https://codesandbox.io/s/react-3d-carousel-wth-framer-motion-rtn6vx?file=/src/App.js