Animated Modals with Framer Motion

Framer Motion is a React library for adding declarative animations to your components. It provides a variety of components that wrap plain HTML elements to extend them with animation superpowers 🦸. In this lesson, we will build a modal with Framer Motion with a variety of different animations styles.

🚀 Try out the live demo

Setup

Installation

Create a new React project

command line
$ npx create-react-app framer-demo

Open your new React app

$ cd react-framer-demo

Install the Framer Motion package

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

App.js
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>
  )
}

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.

components/Backdrop/index.js
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;

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.

components/Modal/index.js
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;

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.

App.js
<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>

Extra Animations

The full demo provides a variety of animations to try out. Here are some of the animations found in the full source code.

Flip

Flip the modal in and out.

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,
    },
  },
};

Newspaper

Extra, extra, read all about it!

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,
    },
  },
};

Bad Suspension

Make it look like something is broken, great for error messages.

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,
  },
};

Up Next

Check out the more advanced Framer Motion Notification Feed tutorial.

Questions? Let's chat

Open Discord