DEV Community

Satel
Satel

Posted on

Animated Modal with Framer Motion

Framer Motion is a React library for adding declarative animations to your React applications. It provides a variety of components that wrap plain HTML elements to extend them with animation superpowers.

I would like to build a modal with Framer Motion with a variety of different animation styles.

Setup

Install Framer motion package in your React application

npm i framer-motion

Animated Modal

Trigger Button

Create a button that can when clicked will open a modal. Define a stateful value modalOpen to keep track of the open/close state. In this example, we use the motion.div component also animate the button itself.

function App() {
  const [modalOpen, setModalOpen] = useState(false);

  const close = () => setModalOpen(false);
  const open = () => setModalOpen(true);

  return (
    <div>
      <motion.button
        whileHover={{ scale: 1.1 }}
        whileTap={{ scale: 0.9 }}
        className="save-button"
        onClick={() => (modalOpen ? close() : open())}
      >
        Launch modal
      </motion.button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Backdrop

Create a component to serve as the backdrop for the modal. This component will be a motion.div component that fades in and out. It takes the onClick prop to close the modal when the backdrop is clicked.

import { motion } from "framer-motion";

const Backdrop = ({ children, onClick }) => {

  return (
    <motion.div
      onClick={onClick}
      className="backdrop"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      {children}
    </motion.div>
  );
};

export default Backdrop;
Enter fullscreen mode Exit fullscreen mode

Modal Window

The modal component uses the Backdrop, then has its own motion.div component that animates in and out. The animation states are defined in the dropIn object. Framer will transition from one state to the other when it is mounted in the DOM.

Also notice how stopPropagation is called when the modal is clicked - this prevents it from closing when the modal is clicked.

import { motion } from "framer-motion";
import Backdrop from "../Backdrop";

const dropIn = {
    hidden: {
      y: "-100vh",
      opacity: 0,
    },
    visible: {
      y: "0",
      opacity: 1,
      transition: {
        duration: 0.1,
        type: "spring",
        damping: 25,
        stiffness: 500,
      },
    },
    exit: {
      y: "100vh",
      opacity: 0,
    },
  };


const Modal = ({ handleClose, text }) => {

    return (
      <Backdrop onClick={handleClose}>
          <motion.div
            onClick={(e) => e.stopPropagation()}  
            className="modal orange-gradient"
            variants={dropIn}
            initial="hidden"
            animate="visible"
            exit="exit"
          >
            <p>{text}</p>
            <button onClick={handleClose}>Close</button>
          </motion.div>
      </Backdrop>
    );
  };


export default Modal;
Enter fullscreen mode Exit fullscreen mode

Animate Presence

Framer Motion has a built in AnimatePresence component that can handle animations for components that get added/removed from the DOM - we need it to animate the removal of a modal component.

<AnimatePresence
    // Disable any initial animations on children that
    // are present when the component is first rendered
    initial={false}
    // Only render one component at a time.
    // The exiting component will finish its exit
    // animation before entering component is rendered
    exitBeforeEnter={true}
    // Fires when all exiting nodes have completed animating out
    onExitComplete={() => null}
>
    {modalOpen && <Modal modalOpen={modalOpen} handleClose={close} />}
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode

Extra Animations

Flip

const flip = {
  hidden: {
    transform: "scale(0) rotateX(-360deg)",
    opacity: 0,
    transition: {
      delay: 0.3,
    },
  },
  visible: {
    transform: " scale(1) rotateX(0deg)",
    opacity: 1,
    transition: {
      duration: 0.5,
    },
  },
  exit: {
    transform: "scale(0) rotateX(360deg)",
    opacity: 0,
    transition: {
      duration: 0.5,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Newspaper

const newspaper = {
  hidden: {
    transform: "scale(0) rotate(720deg)",
    opacity: 0,
    transition: {
      delay: 0.3,
    },
  },
  visible: {
    transform: " scale(1) rotate(0deg)",
    opacity: 1,
    transition: {
      duration: 0.5,
    },
  },
  exit: {
    transform: "scale(0) rotate(-720deg)",
    opacity: 0,
    transition: {
      duration: 0.3,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Bad Suspension

const badSuspension = {
  hidden: {
    y: "-100vh",
    opacity: 0,
    transform: "scale(0) rotateX(-360deg)",
  },
  visible: {
    y: "-25vh",
    opacity: 1,
    transition: {
      duration: 0.2,
      type: "spring",
      damping: 15,
      stiffness: 500,
    },
  },
  exit: {
    y: "-100vh",
    opacity: 0,
  },
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)