I was trying to implement Framer Motion, to understand how it works, in a personal project of mine.
For now, I wrote the following code (100% working), in these two components:
-
Modal.jsx
import { createPortal } from "react-dom"; import { motion } from "framer-motion"; export default function Modal({ title, children, onClose }) { return createPortal( <> <div className="backdrop" onClick={onClose} /> <motion.dialog variants={{ open: { opacity: 1, translateY: 0 }, closed: { opacity: 0, translateY: 30 }, }} initial="closed" animate="open" exit="closed" open className="modal" > <h2>{title}</h2> {children} </motion.dialog> </>, document.getElementById("modal") ); }
-
NewChallenge.jsx
import { useContext, useRef, useState } from "react"; import { motion } from "framer-motion"; import { ChallengesContext } from "../store/challenges-context.jsx"; import Modal from "./Modal.jsx"; import images from "../assets/images.js"; export default function NewChallenge({ onDone }) { const title = useRef(); const description = useRef(); const deadline = useRef(); const [selectedImage, setSelectedImage] = useState(null); const { addChallenge } = useContext(ChallengesContext); function handleSelectImage(image) { setSelectedImage(image); } function handleSubmit(event) { event.preventDefault(); const challenge = { title: title.current.value, description: description.current.value, deadline: deadline.current.value, image: selectedImage, }; if ( !challenge.title.trim() || !challenge.description.trim() || !challenge.deadline.trim() || !challenge.image ) { return; } onDone(); addChallenge(challenge); } return ( <Modal title="New Challenge" onClose={onDone}> <form id="new-challenge" onSubmit={handleSubmit}> <p> <label htmlFor="title">Title</label> <input ref={title} type="text" name="title" id="title" /> </p> <p> <label htmlFor="description">Description</label> <textarea ref={description} name="description" id="description" /> </p> <p> <label htmlFor="deadline">Deadline</label> <input ref={deadline} type="date" name="deadline" id="deadline" /> </p> <motion.ul id="new-challenge-images" variants={{ open: { transition: { staggerChildren: 0.05 } }, }} initial="closed" animate="open" > {images.map((image) => ( <motion.li key={image.alt} onClick={() => handleSelectImage(image)} className={selectedImage === image ? "selected" : undefined} variants={{ closed: { opacity: 0, scale: 0 }, open: { opacity: 1, scale: 1 }, }} > <img {...image} /> </motion.li> ))} </motion.ul> <p className="new-challenge-actions"> <button type="button" onClick={onDone}> Cancel </button> <button>Add Challenge</button> </p> </form> </Modal> ); }
All animations, after various tests, work correctly, in particular:
-
The animation in Modal.jsx, animates the <dialog> with a fade-in-from-bottom style animation
-
The animations in New Challenge.jsx animates the list <ul> by setting an animation delay on the elements of the list with
staggerChildren
and the list items <li> with a bounce style animation.
As mentioned above, everything works and the animations are triggered correctly.
What I wanted to understand is, where does the list <ul> get the initial state “closed” from? From the Modal?
Is there a better way to do what I’m trying to do?
I am not an expert in using this library, but I find it very useful and would like to learn how to use it in the best possible way.
Could anyone help me understand?