├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── arr-utils.js ├── components ├── Backdrop │ └── index.jsx ├── Input │ └── index.jsx ├── Modal │ └── index.jsx └── Notification │ └── index.jsx ├── hooks └── useModal.jsx ├── index.css ├── index.js └── stateLogger.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creating modals in React Framer Motion 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/78eb75d8-48e8-4356-b2d9-1247acc0e97a/deploy-status)](https://app.netlify.com/sites/react-framer-demo/deploys) 4 | 5 | 1. [Features](#features-include) 6 | 2. [Installation](#installation) 7 | 3. [Set up](#set-up) 8 | 4. [Live Demo](https://react-framer-demo.netlify.app) 9 | 10 | ## What is Framer Motion? 11 | 12 | Framer Motion is a relatively new open source, production-ready animation library for React developers. 13 | 14 | [Framer Motion docs](https://framer.com/api/motion) 15 | 16 | ### Features include: 17 | 18 | - Spring animations 19 | - Simple keyframes syntax 20 | - Gestures (drag/tap/hover) 21 | - Layout and shared layout animations 22 | - SVG paths 23 | - Exit animations 24 | - Server-side rendering 25 | - Variants that orchestrate animations across components 26 | - CSS variables 27 | 28 | ## Installation 29 | 30 | Create a new React project 31 | 32 | ```sh 33 | $ npx create-react-app framer-demo 34 | ``` 35 | 36 | Open your new React app 37 | 38 | ```sh 39 | $ cd react-framer-demo 40 | ``` 41 | 42 | Install the Framer Motion package 43 | 44 | ```sh 45 | $ npm i framer-motion 46 | ``` 47 | 48 | ## Set up 49 | 50 | #### Project structure 51 | 52 | ``` 53 | framer-demo 54 | ├── README.md 55 | ├── node_modules 56 | ├── package.json 57 | ├── .gitignore 58 | ├── public 59 | │ ├── favicon.ico 60 | │ ├── index.html 61 | │ ├── logo192.png 62 | │ ├── logo512.png 63 | │ ├── manifest.json 64 | │ └── robots.txt 65 | └── src 66 | ├── hooks 67 | │ └── useModal.jsx 68 | ├── components 69 | │ ├── Modal 70 | │ │ └── index.jsx 71 | │ ├── Backdrop 72 | │ │ └── index.jsx 73 | │ ├── Notification 74 | │ │ └── index.jsx 75 | │ └── Input 76 | │ └── index.jsx 77 | ├── App.jsx 78 | ├── stateLogger.js 79 | ├── arr-utils.js 80 | ├── index.css 81 | └── index.js 82 | ``` 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-framer-demo", 3 | "version": "0.1.2", 4 | "private": true, 5 | "dependencies": { 6 | "framer-motion": "^4.1.17", 7 | "react": "^17.0.2", 8 | "react-dom": "^17.0.2", 9 | "react-scripts": "4.0.3" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": [ 19 | "react-app" 20 | ] 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireship-io/framer-demo/d844d255d607e8df1d230431f8090cb22d655a89/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Framer Motion demo 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireship-io/framer-demo/d844d255d607e8df1d230431f8090cb22d655a89/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireship-io/framer-demo/d844d255d607e8df1d230431f8090cb22d655a89/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Framer Motion Demo", 3 | "name": "React Framer Motion Demo", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#030303", 24 | "background_color": "#030303" 25 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | import useModal from "./hooks/useModal"; 4 | import { framerLogger } from "./stateLogger"; 5 | import Notification from "./components/Notification"; 6 | import Input from "./components/Input"; 7 | import Modal from "./components/Modal"; 8 | import { add } from "./arr-utils"; 9 | 10 | function App() { 11 | // Modal state 12 | const { modalOpen, close, open } = useModal(); 13 | 14 | // Modal type 15 | const [modalType, setModalType] = useState("dropIn"); 16 | const handleType = (e) => setModalType(e.target.value); 17 | 18 | // Notifications state 19 | const [notifications, setNotifications] = useState([]); 20 | 21 | // Notification text 22 | const [text, setText] = useState("Awesome job! 🚀"); 23 | const handleText = (e) => setText(e.target.value); 24 | 25 | // Notification style 26 | const [style, setStyle] = useState("success"); 27 | const handleStyle = (e) => setStyle(e.target.value); 28 | 29 | // Notification position 30 | const [position, setPosition] = useState("bottom"); 31 | const handlePosition = (e) => setPosition(e.target.value); 32 | 33 | return ( 34 | <> 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 53 | Launch modal 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | 66 | 67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | setNotifications(add(notifications, text, style))} 89 | > 90 | + Stack em up 91 | 92 | 93 | 94 | 95 | {modalOpen && ( 96 | 97 | )} 98 | 99 | 100 | 101 | {notifications && 102 | notifications.map((notification) => ( 103 | 109 | ))} 110 | 111 | 112 | ); 113 | } 114 | 115 | const Header = () => ( 116 | 117 | Framer Motion 118 | ⚛️ React 119 | 120 | ); 121 | 122 | const SubHeader = ({ text }) => {text}; 123 | 124 | const ModalContainer = ({ children, label }) => ( 125 | // Enables the animation of components that have been removed from the tree 126 | framerLogger(label)} 136 | > 137 | {children} 138 | 139 | ); 140 | 141 | const NotificationContainer = ({ children, position }) => { 142 | return ( 143 |
144 |
    145 | framerLogger("Notifications container")} 148 | > 149 | {children} 150 | 151 |
152 |
153 | ); 154 | }; 155 | 156 | export default App; 157 | -------------------------------------------------------------------------------- /src/arr-utils.js: -------------------------------------------------------------------------------- 1 | // MacGuyver'd utility to generate && remove notifications 2 | export const remove = (arr, item) => { 3 | const newArr = [...arr]; 4 | newArr.splice( 5 | newArr.findIndex((i) => i === item), 6 | 1 7 | ); 8 | return newArr; 9 | }; 10 | 11 | let newIndex = 0; 12 | export const add = (arr, text, style) => { 13 | newIndex = newIndex + 1; 14 | return [...arr, { id: newIndex, text: text, style: style }]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Backdrop/index.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { stateLogger } from "../../stateLogger"; 3 | import { motion } from "framer-motion"; 4 | 5 | const Backdrop = ({ children, onClick }) => { 6 | // Log state 7 | useEffect(() => { 8 | stateLogger("Backdrop", true); 9 | return () => stateLogger("Backdrop", false); 10 | }, []); 11 | 12 | return ( 13 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export default Backdrop; 26 | -------------------------------------------------------------------------------- /src/components/Input/index.jsx: -------------------------------------------------------------------------------- 1 | const Input = ({ onChange, value, placeHolder }) => ( 2 | 9 | ); 10 | 11 | export default Input; 12 | -------------------------------------------------------------------------------- /src/components/Modal/index.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { motion } from "framer-motion"; 3 | import { stateLogger } from "../../stateLogger"; 4 | import Backdrop from "../Backdrop/index"; 5 | 6 | const dropIn = { 7 | hidden: { 8 | y: "-100vh", 9 | opacity: 0, 10 | }, 11 | visible: { 12 | y: "0", 13 | opacity: 1, 14 | transition: { 15 | duration: 0.1, 16 | type: "spring", 17 | damping: 25, 18 | stiffness: 500, 19 | }, 20 | }, 21 | exit: { 22 | y: "100vh", 23 | opacity: 0, 24 | }, 25 | }; 26 | 27 | const flip = { 28 | hidden: { 29 | transform: "scale(0) rotateX(-360deg)", 30 | opacity: 0, 31 | transition: { 32 | delay: 0.3, 33 | }, 34 | }, 35 | visible: { 36 | transform: " scale(1) rotateX(0deg)", 37 | opacity: 1, 38 | transition: { 39 | duration: 0.5, 40 | }, 41 | }, 42 | exit: { 43 | transform: "scale(0) rotateX(360deg)", 44 | opacity: 0, 45 | transition: { 46 | duration: 0.5, 47 | }, 48 | }, 49 | }; 50 | 51 | const newspaper = { 52 | hidden: { 53 | transform: "scale(0) rotate(720deg)", 54 | opacity: 0, 55 | transition: { 56 | delay: 0.3, 57 | }, 58 | }, 59 | visible: { 60 | transform: " scale(1) rotate(0deg)", 61 | opacity: 1, 62 | transition: { 63 | duration: 0.5, 64 | }, 65 | }, 66 | exit: { 67 | transform: "scale(0) rotate(-720deg)", 68 | opacity: 0, 69 | transition: { 70 | duration: 0.3, 71 | }, 72 | }, 73 | }; 74 | 75 | const badSuspension = { 76 | hidden: { 77 | y: "-100vh", 78 | opacity: 0, 79 | transform: "scale(0) rotateX(-360deg)", 80 | }, 81 | visible: { 82 | y: "-25vh", 83 | opacity: 1, 84 | transition: { 85 | duration: 0.2, 86 | type: "spring", 87 | damping: 15, 88 | stiffness: 500, 89 | }, 90 | }, 91 | exit: { 92 | y: "-100vh", 93 | opacity: 0, 94 | }, 95 | }; 96 | 97 | const gifYouUp = { 98 | hidden: { 99 | opacity: 0, 100 | scale: 0, 101 | }, 102 | visible: { 103 | opacity: 1, 104 | scale: 1, 105 | transition: { 106 | duration: 0.2, 107 | ease: "easeIn", 108 | }, 109 | }, 110 | exit: { 111 | opacity: 0, 112 | scale: 0, 113 | transition: { 114 | duration: 0.15, 115 | ease: "easeOut", 116 | }, 117 | }, 118 | }; 119 | 120 | const Modal = ({ handleClose, text, type }) => { 121 | // Log state 122 | useEffect(() => { 123 | stateLogger("Modal", true); 124 | return () => stateLogger("Modal", false); 125 | }, []); 126 | 127 | return ( 128 | 129 | {type === "dropIn" && ( 130 | e.stopPropagation()} // Prevent click from closing modal 132 | className="modal orange-gradient" 133 | variants={dropIn} 134 | initial="hidden" 135 | animate="visible" 136 | exit="exit" 137 | > 138 | 139 | 140 | 141 | )} 142 | 143 | {type === "flip" && ( 144 | e.stopPropagation()} 146 | className="modal orange-gradient" 147 | variants={flip} 148 | initial="hidden" 149 | animate="visible" 150 | exit="exit" 151 | > 152 | 153 | 154 | 155 | )} 156 | 157 | {type === "newspaper" && ( 158 | e.stopPropagation()} 160 | className="modal orange-gradient" 161 | variants={newspaper} 162 | initial="hidden" 163 | animate="visible" 164 | exit="exit" 165 | > 166 | 167 | 168 | 169 | )} 170 | 171 | {type === "badSuspension" && ( 172 | e.stopPropagation()} 174 | className="modal orange-gradient" 175 | variants={badSuspension} 176 | initial="hidden" 177 | animate="visible" 178 | exit="exit" 179 | > 180 | 181 | 182 | 183 | 184 | )} 185 | 186 | {type === "gifYouUp" && ( 187 | e.stopPropagation()} 190 | style={{ 191 | padding: 0, 192 | height: "auto", 193 | width: "auto", 194 | display: "flex", 195 | justifyContent: "center", 196 | }} 197 | variants={gifYouUp} 198 | initial="hidden" 199 | animate="visible" 200 | exit="exit" 201 | > 202 |

212 | Tap x2 to close 213 |

214 | 231 |
232 | )} 233 |
234 | ); 235 | }; 236 | 237 | const ModalText = ({ text }) => ( 238 |
239 |

{text}

240 |
241 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius laboriosam labore, totam 242 | expedita voluptates tempore asperiores sequi, alias cum veritatis, minima dolor iste similique 243 | eos id. Porro, culpa? Officiis, placeat? 244 |
245 |
246 | ); 247 | 248 | const ModalButton = ({ onClick, label }) => ( 249 | 256 | {label} 257 | 258 | ); 259 | 260 | export default Modal; 261 | -------------------------------------------------------------------------------- /src/components/Notification/index.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { remove } from "../../arr-utils"; 3 | 4 | const notificationVariants = { 5 | initial: { 6 | opacity: 0, 7 | y: 50, 8 | scale: 0.2, 9 | transition: { duration: 0.1 }, 10 | }, 11 | animate: { 12 | opacity: 1, 13 | y: 0, 14 | scale: 1, 15 | }, 16 | exit: { 17 | opacity: 0, 18 | scale: 0.2, 19 | transition: { ease: "easeOut", duration: 0.15 }, 20 | }, 21 | hover: { scale: 1.05, transition: { duration: 0.1 } }, 22 | }; 23 | 24 | const Notification = ({ notifications, setNotifications, notification }) => { 25 | const { text, style } = notification; 26 | 27 | const handleClose = () => setNotifications(remove(notifications, notification)); 28 | 29 | const styleType = () => { 30 | // Controlled by selection menu 31 | switch (style) { 32 | case "success": 33 | return { background: "linear-gradient(15deg, #6adb00, #04e800)" }; 34 | case "error": 35 | return { background: "linear-gradient(15deg, #ff596d, #d72c2c)" }; 36 | case "warning": 37 | return { background: "linear-gradient(15deg, #ffac37, #ff9238)" }; 38 | case "light": 39 | return { background: "linear-gradient(15deg, #e7e7e7, #f4f4f4)" }; 40 | default: 41 | return { background: "linear-gradient(15deg, #202121, #292a2d)" }; 42 | } 43 | }; 44 | 45 | // const closeOnDrag = (event, info) => { 46 | // console.log(info) 47 | // if (info.velocity.x > 0) { 48 | // handleClose(); 49 | // } 50 | // } 51 | 52 | return ( 53 | 67 |

68 | {text} 69 |

70 | 71 |
72 | ); 73 | }; 74 | 75 | const Path = (props) => ( 76 | 83 | ); 84 | 85 | const CloseButton = ({ handleClose, color }) => ( 86 | 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | 94 | export default Notification; 95 | -------------------------------------------------------------------------------- /src/hooks/useModal.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | // Centralizes modal control 4 | const useModal = () => { 5 | const [modalOpen, setModalOpen] = useState(false); 6 | 7 | const close = () => setModalOpen(false); 8 | const open = () => setModalOpen(true); 9 | 10 | return { modalOpen, close, open }; 11 | }; 12 | 13 | export default useModal; 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import-normalize; 2 | @import url(https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600&display=swap); 3 | 4 | :root { 5 | --dark: #101315; 6 | --light: #eeeeee; 7 | --gradient: linear-gradient(10deg, #ffaa00, #ff6a00); 8 | --gradient2: linear-gradient(15deg, #04ea00, #00d17d); 9 | --gradient3: linear-gradient(15deg, #b648ff, #ef5dff); 10 | font-size: 1rem; 11 | } 12 | 13 | * { 14 | box-sizing: border-box; 15 | } 16 | 17 | body { 18 | background: var(--dark); 19 | font-family: "Montserrat", sans-serif; 20 | height: 100%; 21 | overflow-y: hidden; 22 | } 23 | 24 | main { 25 | margin: auto; 26 | display: flex; 27 | flex-direction: column; 28 | padding: 2rem; 29 | align-items: center; 30 | overflow-y: hidden; 31 | } 32 | 33 | h1 { 34 | color: #ff9d00; 35 | font-size: 175%; 36 | font-weight: 400; 37 | font-family: "Montserrat", sans-serif; 38 | } 39 | 40 | h2 { 41 | font-weight: 500; 42 | font-size: 150%; 43 | letter-spacing: 0; 44 | font-family: "Montserrat", sans-serif; 45 | } 46 | 47 | h3 { 48 | text-align: center; 49 | color: var(--dark); 50 | font-weight: 700; 51 | font-size: 2rem; 52 | letter-spacing: 1px; 53 | font-family: "Montserrat", sans-serif; 54 | padding: 2rem 0 1.5rem 0; 55 | margin: 0; 56 | text-transform: capitalize; 57 | } 58 | 59 | h5 { 60 | text-align: center; 61 | color: var(--dark); 62 | font-weight: 500; 63 | font-size: 130%; 64 | text-align: justify; 65 | letter-spacing: 0; 66 | font-family: "Montserrat", sans-serif; 67 | padding: 0; 68 | line-height: 125%; 69 | margin: 0; 70 | } 71 | 72 | kbd { 73 | background: #eee; 74 | border-radius: 3px; 75 | border: 1px solid #b4b4b4; 76 | box-shadow: 0 1px 1px #00000028, 0 2px 0 0 #ffffffc3 inset; 77 | color: #131313; 78 | display: inline-block; 79 | font-size: 0.85em; 80 | font-weight: 700; 81 | line-height: 1; 82 | padding: 2px 4px; 83 | white-space: nowrap; 84 | margin: auto; 85 | } 86 | 87 | button { 88 | width: auto; 89 | height: 3rem; 90 | border: none; 91 | outline: none; 92 | -webkit-appearance: none; 93 | border-radius: 4px; 94 | font-weight: 600; 95 | font-size: 1.25rem; 96 | letter-spacing: 1.25px; 97 | cursor: default; 98 | font-family: "Montserrat", sans-serif; 99 | } 100 | 101 | .backdrop { 102 | position: absolute; 103 | top: 0; 104 | left: 0; 105 | height: 100%; 106 | width: 100%; 107 | background: #000000e1; 108 | display: flex; 109 | align-items: center; 110 | justify-content: center; 111 | overflow-y: hidden; 112 | } 113 | 114 | .modal { 115 | width: clamp(50%, 700px, 90%); 116 | height: min(50%, 300px); 117 | 118 | margin: auto; 119 | padding: 0 2rem; 120 | border-radius: 12px; 121 | display: flex; 122 | flex-direction: column; 123 | align-items: center; 124 | } 125 | 126 | .orange-gradient { 127 | background: var(--gradient); 128 | } 129 | 130 | .green-gradient { 131 | background: var(--gradient2); 132 | } 133 | 134 | .pink { 135 | color: #c273ff; 136 | } 137 | 138 | .gray { 139 | color: #666666; 140 | } 141 | 142 | .light-blue { 143 | color: #00b7ff; 144 | } 145 | 146 | .modal-button { 147 | position: relative; 148 | bottom: 1.5rem; 149 | padding: 0 3rem; 150 | min-height: 3rem; 151 | margin: auto auto 0 auto; 152 | background: var(--dark); 153 | color: var(--light); 154 | } 155 | 156 | .save-button { 157 | padding: 0 1rem; 158 | margin: 2rem auto auto 0; 159 | background: var(--gradient); 160 | color: var(--dark); 161 | } 162 | 163 | .close-button { 164 | padding: 0 2rem; 165 | height: 2.5rem; 166 | margin: 2rem auto 1rem 0; 167 | background: #101111; 168 | /* border: 1px dashed #9a9a9a99; */ 169 | color: #ffaa00; 170 | border-radius: 4px; 171 | transition: background ease 400ms; 172 | box-shadow: 1px 1px 15px #03030399; 173 | } 174 | 175 | .input, 176 | input { 177 | width: calc(100vw - 5rem); 178 | height: 3rem; 179 | margin: 0 auto 0 0; 180 | padding: 0.25rem 0.5rem; 181 | outline: none; 182 | background: var(--dark); 183 | border: 2px solid #ff9d00; 184 | border-radius: 6px; 185 | color: #ff9d00; 186 | font-size: 1.25rem; 187 | font-weight: 400; 188 | font-family: "Montserrat", sans-serif; 189 | } 190 | 191 | ::placeholder { 192 | font-style: italic; 193 | } 194 | 195 | .container { 196 | display: flex; 197 | width: 50vw; 198 | height: 50%; 199 | margin: auto; 200 | } 201 | 202 | ul, 203 | li { 204 | padding: 0; 205 | margin: 0; 206 | } 207 | 208 | ul { 209 | position: fixed; 210 | bottom: 0.5rem; 211 | right: 0; 212 | top: 0.5rem; 213 | list-style: none; 214 | display: flex; 215 | flex-direction: column; 216 | justify-content: flex-end; 217 | } 218 | 219 | .bottom { 220 | justify-content: flex-end; 221 | } 222 | 223 | .top { 224 | justify-content: flex-start; 225 | } 226 | 227 | li { 228 | width: 225px; 229 | margin: 0.5rem 1.5rem; 230 | padding: 0 1rem; 231 | height: 3rem; 232 | display: flex; 233 | align-items: center; 234 | justify-content: center; 235 | position: relative; 236 | border-radius: 4px; 237 | } 238 | 239 | .notification-text { 240 | margin: auto auto auto 0; 241 | padding: 0; 242 | font-size: 100%; 243 | font-weight: 600; 244 | letter-spacing: 0.25px; 245 | font-family: "Montserrat", sans-serif; 246 | } 247 | 248 | .add-button { 249 | padding: 0 1rem; 250 | margin: 2rem auto auto 0; 251 | background: var(--gradient2); 252 | color: var(--dark); 253 | } 254 | 255 | .close { 256 | height: 1.1rem; 257 | background: transparent; 258 | border: none; 259 | outline: none; 260 | margin: 0 0 0 auto; 261 | padding: 0; 262 | display: flex; 263 | align-items: center; 264 | justify-content: center; 265 | } 266 | 267 | .close svg { 268 | margin: 0 auto; 269 | width: 100%; 270 | height: 100%; 271 | } 272 | 273 | .sub-header { 274 | margin: 1rem auto 1rem 0; 275 | color: #9e9e9e; 276 | } 277 | 278 | @media screen and (min-width: 960px) { 279 | button { 280 | cursor: pointer; 281 | } 282 | /* .modal { 283 | width: 750px; 284 | height: 300px; 285 | } */ 286 | .input, 287 | input { 288 | width: calc(25vw); 289 | } 290 | h1 { 291 | color: #ff9d00; 292 | font-size: 250%; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const root = document.getElementById("root"); 7 | 8 | render(, root); 9 | -------------------------------------------------------------------------------- /src/stateLogger.js: -------------------------------------------------------------------------------- 1 | // Tool for tracking and labeling animation/component state 2 | const log = console.log; 3 | export const framerLogger = (label) => log(`%c${label}: animation complete`, "color: red"); 4 | export const stateLogger = (label, mounted) => { 5 | mounted 6 | ? log(`%c${label}: mounted`, "color: green") 7 | : log(`%c${label}: unmounted`, "color: orange"); 8 | }; 9 | --------------------------------------------------------------------------------