├── .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 | [](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 | You need to enable JavaScript to run this app.
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 | 🪂 Drop in
41 | 🛹 Flip
42 | 🗞 Newspaper
43 | 🔩 Bad Suspension
44 | 🎸 GIF you up
45 |
46 |
47 |
53 | Launch modal
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
66 |
67 |
68 |
69 |
70 | ✅ Success
71 | ⚠️ Warning
72 | 🛑 Error
73 | ☀️ Light
74 | 🌙 Dark
75 |
76 |
77 |
78 |
79 |
80 | 👇🏼 Bottom
81 | ☝🏼 Top
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 |
--------------------------------------------------------------------------------