├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build-deploy-example.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets └── logo.png ├── example ├── .gitignore ├── README.md ├── bash.exe.stackdump ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── assets │ │ ├── arrow-down.svg │ │ ├── chevron-down.svg │ │ ├── copy.svg │ │ ├── index.ts │ │ └── npm.svg │ ├── components │ │ ├── ApiTable.tsx │ │ └── Code.tsx │ ├── index.css │ ├── index.tsx │ ├── prism-atom-dark.css │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock ├── package.json ├── rollup.config.js ├── src ├── assets │ └── icons │ │ ├── close.svg │ │ └── index.ts ├── components │ ├── ReactPortal.tsx │ └── index.ts ├── config.ts ├── hooks │ └── useDisableScroll.ts ├── index.tsx ├── styles.scss └── types.ts ├── tsconfig.json ├── types.d.ts └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]: Issue in brief" 5 | labels: bug 6 | assignees: SneakySensei 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]: Brief title for the feature" 5 | labels: enhancement 6 | assignees: SneakySensei 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build-deploy-example.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Demo App 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 16.x 23 | 24 | - name: Install NPM packages 25 | run: | 26 | yarn 27 | yarn build 28 | cd example 29 | yarn 30 | 31 | - name: Build project 32 | run: | 33 | cd example 34 | yarn build 35 | cd build 36 | touch CNAME 37 | echo "react-lean-modal.snehil.dev" >> CNAME 38 | 39 | - name: Upload production-ready build files 40 | uses: actions/upload-artifact@v3 41 | with: 42 | name: production-files 43 | path: ./example/build 44 | 45 | delpoy: 46 | name: Deploy 47 | needs: build 48 | runs-on: ubuntu-latest 49 | if: github.ref == 'refs/heads/main' 50 | 51 | steps: 52 | - name: Download artifact 53 | uses: actions/download-artifact@v3 54 | with: 55 | name: production-files 56 | path: ./build 57 | 58 | - name: Deploy to gh-pages 59 | uses: peaceiris/actions-gh-pages@v3 60 | with: 61 | github_token: ${{ secrets.GITHUB_TOKEN }} 62 | publish_dir: ./build 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Snehil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | React Lean Modal Logo 3 |

4 | 5 | This package exposes a `` component which is fully controlled by a single prop. It comes packed with enter and exit animations for all your motion needs. 6 | 7 | It's currently powered by [react-transition-group](https://reactcommunity.org/react-transition-group/) but I'm planning to turn it into a zero-dependency package in the near future so that it doesn't hurt your [bundle size](https://bundlephobia.com/package/react-lean-modal). 8 | 9 |

10 | GitHub license 11 | npm 12 | npm 13 | Twitter Follow 14 |

15 | 16 | ## 🔧 Installation 17 | 18 | **With NPM** 19 | 20 | ```bash 21 | npm install react-lean-modal 22 | ``` 23 | 24 | **With Yarn** 25 | 26 | ```bash 27 | yarn add react-lean-modal 28 | ``` 29 | 30 | ## 📦 Usage 31 | 32 | Enough talk, show me the code. 33 | 34 | ```js 35 | setShowModal(false)} 41 | titleElement={

Example Modal

} 42 | /> 43 | ``` 44 | 45 | ## 🔌 API 46 | 47 | | Property | Description | Type | Default | 48 | | -------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------ | 49 | | isOpen | Whether the modal is open or closed | `boolean` | `false` | 50 | | onClose | Function that's called when a close action is registered. This is where we set the isOpen prop to false | `() => void` | - | 51 | | children | Content to render inside the modal's content area | `React.ReactNode` | - | 52 | | enterAnimation | The animation to show when modal opens | [AnimationType](#animationtype) | `"zoom-in"` | 53 | | exitAnimation | The animation to show when modal closes. Behaves as the reverse of enterAnimation. | [AnimationType](#animationtype) | `"zoom-in"` | 54 | | timeout | The duration of animations in milliseconds | `number` | `250`(ms) | 55 | | titleElement | Content to render on the left of the modal header | `React.ReactNode` | - | 56 | | closeIcon | Content to render inside the close button | `React.ReactNode` | Included SVG | 57 | | classNames | Additional class names to apply to the modal | `{root?: string, backdrop?: string, content?: string, header?: string, closeButton?: string, body?: string}` | `{}` | 58 | 59 | ### AnimationType 60 | 61 | ` "fade" | "fade-left" | "fade-right" | "fade-top" | "fade-bottom" | "slide-left" | "slide-right" | "slide-top" | "slide-bottom" | "zoom-in" | "zoom-out" | "spin-cw" | "spin-ccw" | "rotate-left" | "rotate-right" | "rotate-top" | "rotate-bottom"` 62 | 63 | ## 🚨 Forking this repo 64 | 65 | Many people have contacted us asking if they can use this code for their own websites. The answer to that question is usually "yes", with attribution. There are some cases, such as using this code for a business or something that is greater than a personal project, that we may be less comfortable saying yes to. If in doubt, please don't hesitate to ask us. 66 | 67 | We value keeping this package open source, but as you all know, plagiarism is bad. We actively spend a non-negligible amount of effort developing, designing, and trying to perfect this iteration of our package, and we are proud of it! All we ask is to not claim this effort as your own. 68 | 69 | So, feel free to fork this repo. If you do, please just give us proper credit by linking back to this repo, [https://github.com/SneakySensei/react-lean-modal](https://github.com/jagnani73/react-easy-marquee/). Refer to this handy [quora](https://www.quora.com/Is-it-bad-to-copy-other-peoples-code) post if you're not sure what to do. Thanks! 70 | 71 | ## 💥 Mention 72 | 73 | Parts of this README is inspired from [https://github.com/jagnani73/react-easy-marquee/](https://github.com/jagnani73/react-easy-marquee/). 74 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SneakySensei/react-lean-modal/83cfcf0a4a7be50631f1197ec9a92611ff5d76b6/assets/logo.png -------------------------------------------------------------------------------- /example/.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 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /example/bash.exe.stackdump: -------------------------------------------------------------------------------- 1 | Stack trace: 2 | Frame Function Args 3 | 000FFFFA310 00210062B0E (002102970D8, 00210275E3E, 000FFFFA310, 000FFFF9210) 4 | 000FFFFA310 0021004846A (00000000000, 00000000000, 00000000000, 00000000000) 5 | 000FFFFA310 002100484A2 (00210297189, 000FFFFA1C8, 000FFFFA310, 00000000000) 6 | 000FFFFA310 002100D2DDE (00000000000, 00000000000, 00000000000, 00000000000) 7 | 000FFFFA310 002100D2F05 (000FFFFA320, 00000000000, 00000000000, 00000000000) 8 | 000FFFFA5E0 002100D44C5 (000FFFFA320, 00000000000, 00000000000, 00000000000) 9 | End of stack trace 10 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lean-modal-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://react-lean-modal.snehil.dev", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^12.0.0", 9 | "@testing-library/user-event": "^13.2.1", 10 | "@types/jest": "^27.0.1", 11 | "@types/node": "^16.7.13", 12 | "@types/prismjs": "^1.26.0", 13 | "@types/react": "^17.0.20", 14 | "@types/react-dom": "^17.0.9", 15 | "prismjs": "^1.29.0", 16 | "react": "link:../node_modules/react", 17 | "react-dom": "link:../node_modules/react-dom", 18 | "react-lean-modal": "link:..", 19 | "react-scripts": "5.0.0", 20 | "typescript": "^4.4.2", 21 | "web-vitals": "^2.1.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "autoprefixer": "^10.4.8", 49 | "postcss": "^8.4.16", 50 | "tailwindcss": "^3.1.8" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SneakySensei/react-lean-modal/83cfcf0a4a7be50631f1197ec9a92611ff5d76b6/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | 33 | React Lean Modal 34 | 35 | 36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SneakySensei/react-lean-modal/83cfcf0a4a7be50631f1197ec9a92611ff5d76b6/example/public/logo192.png -------------------------------------------------------------------------------- /example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SneakySensei/react-lean-modal/83cfcf0a4a7be50631f1197ec9a92611ff5d76b6/example/public/logo512.png -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 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": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Modal, { ANIMATIONS, AnimationType } from 'react-lean-modal'; 3 | 4 | import { ArrowDownIcon, CopyIcon, NpmIcon } from 'assets'; 5 | import Code from 'components/Code'; 6 | import ApiTable from 'components/ApiTable'; 7 | 8 | const code = ( 9 | enterAnimation: string, 10 | exitAnimation: string, 11 | timeout: number 12 | ) => ` setShowModal(false)} 18 | titleElement={

Example Modal

} 19 | /> 20 | `; 21 | 22 | const App = () => { 23 | const [showModal, setShowModal] = useState(false); 24 | const [animation, setAnimation] = useState<{ 25 | enterAnimation: AnimationType; 26 | exitAnimation: AnimationType; 27 | }>({ enterAnimation: 'fade', exitAnimation: 'fade' }); 28 | const [timeout, setTimeout] = useState(250); 29 | 30 | const handleOptionChange: React.ChangeEventHandler = ( 31 | event 32 | ) => { 33 | setAnimation((state) => ({ 34 | ...state, 35 | [event.target.name]: event.target.value, 36 | })); 37 | }; 38 | 39 | const handleTimeoutChange: React.ChangeEventHandler = ( 40 | event 41 | ) => { 42 | setTimeout(Number(event.target.value)); 43 | }; 44 | 45 | return ( 46 | <> 47 |
48 | 54 | 55 | 56 | {/* HERO */} 57 |
58 |
59 |
60 |
61 |
62 |

react-lean-modal

63 | 64 |
65 |
66 | {'//'} with npm 67 | { 69 | window.navigator.clipboard.writeText( 70 | 'npm i react-lean-modal' 71 | ); 72 | }} 73 | className="cursor-pointer group flex text-white justify-between items-center" 74 | > 75 | $ npm i react-lean-modal 76 | 77 | 78 |
79 | {'//'} with yarn 80 | { 82 | window.navigator.clipboard.writeText( 83 | 'yarn add react-lean-modal' 84 | ); 85 | }} 86 | className="cursor-pointer group flex text-white justify-between items-center" 87 | > 88 | $ yarn add react-lean-modal 89 | 90 | 91 |
92 |
93 |
94 | Batteries-included, modal library for React powered by{' '} 95 | 100 | react-transition-group 101 | 102 |
103 |
104 | 108 | 109 | Getting Started 110 | 111 |
112 |
113 | 114 | {/* OTHER SECTIONS */} 115 |
116 | {/* OVERVIEW */} 117 |
118 |

119 | > Overview 120 |

121 |

122 | This package exposes a{' '} 123 | 124 | {''} 125 | {' '} 126 | component which is fully controlled by a single prop. It comes 127 | packed with enter and exit animations for all your motion needs. 128 |

129 |

130 | It's currently powered by{' '} 131 | 136 | react-transition-group 137 | {' '} 138 | but I'm planning to turn it into a zero-dependency package in the 139 | near future so that it doesn't hurt your{' '} 140 | 145 | bundle size 146 | 147 | . 148 |

149 |
150 | {/* Example */} 151 |
152 |

153 | > Example 154 |

155 |
156 |
157 |
158 | 173 | 188 |
189 | 198 | 199 | 207 |
208 | 217 |
218 |
219 | {/* API */} 220 |
221 |

222 | > API 223 |

224 |
225 | 237 | Function that's called when a close action is registered 238 |
239 | This is where we set the isOpen prop to false 240 | , 241 | '() => void', 242 | '-', 243 | ], 244 | [ 245 | 'children', 246 | "Content to render inside the modal's content area", 247 | 'React.ReactNode', 248 | '-', 249 | ], 250 | [ 251 | 'enterAnimation', 252 | 'The animation to show when modal opens', 253 | AnimationType, 254 | '"zoom-in"', 255 | ], 256 | [ 257 | 'exitAnimation', 258 | 'The animation to show when modal closes. Behaves as the reverse of enterAnimation.', 259 | AnimationType, 260 | '"zoom-in"', 261 | ], 262 | [ 263 | 'timeout', 264 | 'The duration of animations in milliseconds', 265 | 'number', 266 | '250(ms)', 267 | ], 268 | [ 269 | 'titleElement', 270 | 'Content to render on the left of the modal header', 271 | 'React.ReactNode', 272 | '-', 273 | ], 274 | [ 275 | 'closeIcon', 276 | 'Content to render inside the close button', 277 | 'React.ReactNode', 278 | 'Included SVG', 279 | ], 280 | [ 281 | 'classNames', 282 | 'Additional class names to apply to the modal', 283 |
284 |                       {JSON.stringify(
285 |                         {
286 |                           'root?': 'string',
287 |                           'backdrop?': 'string',
288 |                           'content?': 'string',
289 |                           'header?': 'string',
290 |                           'closeButton?': 'string',
291 |                           'body?': 'string',
292 |                         },
293 |                         null,
294 |                         2
295 |                       ).replaceAll('"', '')}
296 |                     
, 297 | '{}', 298 | ], 299 | ]} 300 | /> 301 |
302 |

303 | - AnimationType 304 |

305 |
306 | {ANIMATIONS.map((item, index) => ( 307 | 308 | "{item}" 309 | {index !== ANIMATIONS.length - 1 && ' | '} 310 | 311 | ))} 312 |
313 |
314 |
315 | 316 | setShowModal(false)} 321 | titleElement={

Example Modal

} 322 | classNames={{ header: 'bg-accent-light' }} 323 | timeout={timeout} 324 | > 325 |

Look I'm a Modal!

326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 |
334 |
335 | 352 | 353 | ); 354 | }; 355 | 356 | export default App; 357 | -------------------------------------------------------------------------------- /example/src/assets/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /example/src/assets/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/src/assets/copy.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /example/src/assets/index.ts: -------------------------------------------------------------------------------- 1 | export { ReactComponent as ArrowDownIcon } from './arrow-down.svg'; 2 | export { ReactComponent as CopyIcon } from './copy.svg'; 3 | export { ReactComponent as NpmIcon } from './npm.svg'; 4 | -------------------------------------------------------------------------------- /example/src/assets/npm.svg: -------------------------------------------------------------------------------- 1 | 3 | 8 | 10 | -------------------------------------------------------------------------------- /example/src/components/ApiTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | headers: React.ReactNode[]; 5 | rows: React.ReactNode[][]; 6 | }; 7 | 8 | function ApiTable({ headers, rows }: Props) { 9 | return ( 10 | 11 | 12 | 13 | {headers.map((header, index) => ( 14 | 17 | ))} 18 | 19 | 20 | 21 | {rows.map((row, index) => ( 22 | 23 | {row.map((cell, index) => ( 24 | 27 | ))} 28 | 29 | ))} 30 | 31 |
15 | {header} 16 |
25 | {cell} 26 |
32 | ); 33 | } 34 | 35 | export default ApiTable; 36 | -------------------------------------------------------------------------------- /example/src/components/Code.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, memo, useState } from 'react'; 2 | import Prism from 'prismjs'; 3 | import 'prismjs/components/prism-jsx'; 4 | import 'prism-atom-dark.css'; 5 | import { CopyIcon } from 'assets'; 6 | 7 | const Code = ({ 8 | code, 9 | language, 10 | className, 11 | }: { 12 | code: string; 13 | language: string; 14 | className?: string; 15 | }) => { 16 | const [copied, setCopied] = useState(false); 17 | 18 | useEffect(() => { 19 | let timeout: NodeJS.Timeout; 20 | if (copied) { 21 | timeout = setTimeout(() => { 22 | setCopied(false); 23 | }, 2000); 24 | } 25 | return () => { 26 | clearTimeout(timeout); 27 | }; 28 | }, [copied]); 29 | 30 | useEffect(() => { 31 | Prism.highlightAll(); 32 | }, [code, language, className]); 33 | 34 | return ( 35 |
36 |       
37 | {copied ? ( 38 | 'Copied' 39 | ) : ( 40 | { 42 | window.navigator.clipboard.writeText(code); 43 | setCopied(true); 44 | }} 45 | /> 46 | )} 47 |
48 | {code} 49 |
50 | ); 51 | }; 52 | 53 | export default memo(Code); 54 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | 12 | * { 13 | @apply m-0; 14 | } 15 | 16 | html, 17 | body { 18 | @apply font-mono scroll-smooth h-full; 19 | } 20 | 21 | body { 22 | line-height: 1.5; 23 | -webkit-font-smoothing: antialiased; 24 | } 25 | 26 | img, 27 | picture, 28 | video, 29 | canvas, 30 | svg { 31 | @apply block max-w-full; 32 | } 33 | 34 | input, 35 | button, 36 | textarea, 37 | select { 38 | font: inherit; 39 | @apply outline-none; 40 | } 41 | 42 | select { 43 | @apply appearance-none; 44 | print-color-adjust: exact; 45 | background-image: url('./assets/chevron-down.svg'); 46 | background-position: right 0.5rem center; 47 | background-repeat: no-repeat; 48 | background-size: 1.5em 1.5em; 49 | padding-right: 2.5rem; 50 | } 51 | 52 | p, 53 | h1, 54 | h2, 55 | h3, 56 | h4, 57 | h5, 58 | h6 { 59 | @apply break-words; 60 | } 61 | 62 | a { 63 | @apply text-accent-dark underline font-semibold; 64 | } 65 | 66 | #root, 67 | #__next { 68 | @apply isolate; 69 | } 70 | } 71 | 72 | @layer utilities { 73 | .text-h1 { 74 | @apply text-[2.5rem] sm:text-[3.05rem] font-semibold; 75 | font-size: clamp( 76 | 2.5rem, 77 | 1.8125000000000002rem + 2.1999999999999993vw, 78 | 3.05rem 79 | ); 80 | } 81 | 82 | .text-h2 { 83 | @apply text-[2rem] sm:text-[2.44375rem] font-semibold; 84 | font-size: clamp( 85 | 2rem, 86 | 1.4453124999999998rem + 1.7750000000000006vw, 87 | 2.44375rem 88 | ); 89 | } 90 | 91 | .text-h3 { 92 | @apply text-[1.625rem] sm:text-[1.95625rem] font-semibold; 93 | font-size: clamp(1.625rem, 1.2109375rem + 1.3250000000000002vw, 1.95625rem); 94 | } 95 | 96 | .text-h4 { 97 | @apply text-[1.25rem] sm:text-[1.5625rem] font-semibold; 98 | font-size: clamp(1.25rem, 0.859375rem + 1.25vw, 1.5625rem); 99 | } 100 | 101 | .text-body { 102 | @apply text-[0.875rem] sm:text-[1rem]; 103 | font-size: clamp(0.875rem, 0.71875rem + 0.5vw, 1rem); 104 | } 105 | 106 | .text-caption { 107 | @apply text-[0.6875rem] sm:text-[0.75rem]; 108 | font-size: clamp(0.6875rem, 0.609375rem + 0.25vw, 0.75rem); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | // import reportWebVitals from './reportWebVitals'; 5 | import './index.css'; 6 | 7 | const root = createRoot(document.getElementById('root')!); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | // reportWebVitals(); 18 | -------------------------------------------------------------------------------- /example/src/prism-atom-dark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * atom-dark theme for `prism.js` 3 | * Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax 4 | * @author Joe Gibson (@gibsjose) 5 | */ 6 | 7 | code[class*='language-'], 8 | pre[class*='language-'] { 9 | color: #c5c8c6; 10 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 11 | font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace; 12 | direction: ltr; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | word-break: normal; 17 | line-height: 1.5; 18 | 19 | -moz-tab-size: 4; 20 | -o-tab-size: 4; 21 | tab-size: 4; 22 | 23 | -webkit-hyphens: none; 24 | -moz-hyphens: none; 25 | -ms-hyphens: none; 26 | hyphens: none; 27 | } 28 | 29 | /* Code blocks */ 30 | pre[class*='language-'] { 31 | padding: 1em; 32 | margin: 0.5em 0; 33 | overflow: auto; 34 | border-radius: 0.3em; 35 | } 36 | 37 | :not(pre) > code[class*='language-'], 38 | pre[class*='language-'] { 39 | background: #1d1f21; 40 | } 41 | 42 | /* Inline code */ 43 | :not(pre) > code[class*='language-'] { 44 | padding: 0.1em; 45 | border-radius: 0.3em; 46 | } 47 | 48 | .token.comment, 49 | .token.prolog, 50 | .token.doctype, 51 | .token.cdata { 52 | color: #7c7c7c; 53 | } 54 | 55 | .token.punctuation { 56 | color: #c5c8c6; 57 | } 58 | 59 | .namespace { 60 | opacity: 0.7; 61 | } 62 | 63 | .token.property, 64 | .token.keyword, 65 | .token.tag { 66 | color: #96cbfe; 67 | } 68 | 69 | .token.class-name { 70 | color: #ffffb6; 71 | text-decoration: underline; 72 | } 73 | 74 | .token.boolean, 75 | .token.constant { 76 | color: #99cc99; 77 | } 78 | 79 | .token.symbol, 80 | .token.deleted { 81 | color: #f92672; 82 | } 83 | 84 | .token.number { 85 | color: #ff73fd; 86 | } 87 | 88 | .token.selector, 89 | .token.attr-name, 90 | .token.string, 91 | .token.char, 92 | .token.builtin, 93 | .token.inserted { 94 | color: #a8ff60; 95 | } 96 | 97 | .token.variable { 98 | color: #c6c5fe; 99 | } 100 | 101 | .token.operator { 102 | color: #ededed; 103 | } 104 | 105 | .token.entity { 106 | color: #ffffb6; 107 | cursor: help; 108 | } 109 | 110 | .token.url { 111 | color: #96cbfe; 112 | } 113 | 114 | .language-css .token.string, 115 | .style .token.string { 116 | color: #87c38a; 117 | } 118 | 119 | .token.atrule, 120 | .token.attr-value { 121 | color: #f9ee98; 122 | } 123 | 124 | .token.function { 125 | color: #dad085; 126 | } 127 | 128 | .token.regex { 129 | color: #e9c062; 130 | } 131 | 132 | .token.important { 133 | color: #fd971f; 134 | } 135 | 136 | .token.important, 137 | .token.bold { 138 | font-weight: bold; 139 | } 140 | 141 | .token.italic { 142 | font-style: italic; 143 | } 144 | -------------------------------------------------------------------------------- /example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /example/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | mono: ['JetBrains Mono', ...defaultTheme.fontFamily.mono], 9 | }, 10 | colors: { 11 | accent: { 12 | DEFAULT: '#6164ff', 13 | light: '#8A8CFF', 14 | dark: '#383CFF', 15 | }, 16 | }, 17 | keyframes: { 18 | 'fade-zoom-up': { 19 | '0%': { opacity: 0, transform: 'translateY(10rem) scale(0.5)' }, 20 | '100%': { opacity: 1, transform: 'translateY(0rem) scale(1)' }, 21 | }, 22 | }, 23 | animation: { 24 | 'fade-zoom-up': 25 | 'fade-zoom-up 0.5s cubic-bezier(0.39, 0.575, 0.565, 1) both', 26 | }, 27 | boxShadow: { 28 | 'top-sm': 29 | 'rgba(0, 0, 0, 0.1) 0px -4px 6px -1px, rgba(0, 0, 0, 0.06) 0px -2px 4px -1px', 30 | }, 31 | }, 32 | }, 33 | plugins: [], 34 | }; 35 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "./src" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lean-modal", 3 | "main": "dist/index.js", 4 | "types": "dist/index.d.ts", 5 | "homepage": "https://react-lean-modal.snehil.dev", 6 | "repository": "https://github.com/sneakysensei/react-lean-modal", 7 | "author": "Snehil ", 8 | "license": "MIT", 9 | "files": [ 10 | "dist" 11 | ], 12 | "peerDependencies": { 13 | "react": "^16.8.0", 14 | "react-dom": "^16.8.0" 15 | }, 16 | "devDependencies": { 17 | "@rollup/plugin-commonjs": "^22.0.2", 18 | "@rollup/plugin-node-resolve": "^13.3.0", 19 | "@svgr/rollup": "^6.3.1", 20 | "@types/react": "^18.0.17", 21 | "@types/react-dom": "^18.0.6", 22 | "@types/react-transition-group": "^4.4.5", 23 | "babel-core": "^6.26.3", 24 | "babel-runtime": "^6.26.0", 25 | "nanoid": "^4.0.0", 26 | "node-sass": "^7.0.1", 27 | "postcss": "^8.4.16", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-transition-group": "^4.4.5", 31 | "rollup": "^2.78.1", 32 | "rollup-plugin-postcss": "^4.0.2", 33 | "rollup-plugin-typescript2": "^0.33.0", 34 | "typescript": "^4.8.2" 35 | }, 36 | "scripts": { 37 | "build": "rollup -c", 38 | "start": "rollup -c -w" 39 | }, 40 | "version": "1.2.1" 41 | } 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import svgr from '@svgr/rollup'; 3 | import postcss from 'rollup-plugin-postcss'; 4 | import nodeResolve from '@rollup/plugin-node-resolve'; 5 | import commonjs from '@rollup/plugin-commonjs'; 6 | 7 | import pkg from './package.json'; 8 | 9 | export default { 10 | input: 'src/index.tsx', 11 | output: [ 12 | { 13 | file: pkg.main, 14 | format: 'cjs', 15 | exports: 'named', 16 | sourcemap: true, 17 | strict: false, 18 | }, 19 | ], 20 | plugins: [ 21 | postcss({ extract: false, modules: true, use: ['sass'] }), 22 | svgr({ icon: true, typescript: true }), 23 | nodeResolve(), 24 | typescript(), 25 | commonjs({ extensions: ['.js', '.ts'] }), 26 | ], 27 | external: ['react', 'react-dom'], 28 | }; 29 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CloseIcon } from './close.svg'; 2 | -------------------------------------------------------------------------------- /src/components/ReactPortal.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from "react"; 2 | import { createPortal } from "react-dom"; 3 | import { customAlphabet } from "nanoid/non-secure"; 4 | 5 | type ReactPortalProps = { 6 | children: React.ReactNode; 7 | }; 8 | 9 | const ReactPortal = ({ children }: ReactPortalProps) => { 10 | const [wrapperElement, setWrapperElement] = useState( 11 | null 12 | ); 13 | 14 | useLayoutEffect(() => { 15 | if (wrapperElement) return; 16 | 17 | const elementId = generateElementId(); 18 | let element = document.getElementById(elementId); 19 | let systemCreated = false; 20 | 21 | if (!element) { 22 | element = createPortalAndAppend(elementId); 23 | systemCreated = true; 24 | } 25 | 26 | setWrapperElement(element); 27 | 28 | return () => { 29 | if (systemCreated && element?.parentNode) { 30 | element.parentNode.removeChild(element); 31 | } 32 | }; 33 | }, []); 34 | 35 | if (!wrapperElement) return null; 36 | return createPortal(children, wrapperElement); 37 | }; 38 | 39 | export default ReactPortal; 40 | 41 | // PRIVATE UTILS 42 | const generateElementId = customAlphabet( 43 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 44 | 6 45 | ); 46 | 47 | const createPortalAndAppend = (id: string) => { 48 | const portalEl = document.createElement("div"); 49 | portalEl.id = id; 50 | portalEl.tabIndex = -1; 51 | document.body.appendChild(portalEl); 52 | return portalEl; 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ReactPortal } from "./ReactPortal"; 2 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const ANIMATIONS = [ 2 | 'fade', 3 | 'fade-left', 4 | 'fade-right', 5 | 'fade-top', 6 | 'fade-bottom', 7 | 'slide-left', 8 | 'slide-right', 9 | 'slide-top', 10 | 'slide-bottom', 11 | 'zoom-in', 12 | 'zoom-out', 13 | 'spin-cw', 14 | 'spin-ccw', 15 | 'rotate-left', 16 | 'rotate-right', 17 | 'rotate-top', 18 | 'rotate-bottom', 19 | ] as const; 20 | -------------------------------------------------------------------------------- /src/hooks/useDisableScroll.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | /** 4 | * Hook to disable scroll on `` as long as the component is mounted. 5 | * One can also render it conditionally to toggle scroll. 6 | */ 7 | const useDisableScroll = () => { 8 | useEffect(() => { 9 | const previousOverflow = document.body.style.overflow ?? 'auto'; 10 | const previousTouchAction = document.body.style.overflow ?? 'auto'; 11 | const previousOverflowScrolling = 12 | document.body.style['-webkit-overflow-scrolling'] ?? 'auto'; 13 | const previousOverscrollBehavior = 14 | document.body.style.overscrollBehavior ?? 'auto'; 15 | 16 | document.body.style.overflow = 'hidden'; 17 | document.body.style.touchAction = 'none'; 18 | document.body.style['-webkit-overflow-scrolling'] = 'none'; 19 | document.body.style.overscrollBehavior = 'none'; 20 | 21 | // Add stable scrollbar gutter to prevent layout shift 22 | document.documentElement.style.scrollbarGutter = 'stable'; 23 | 24 | return () => { 25 | document.body.style.overflow = previousOverflow; 26 | document.body.style.touchAction = previousTouchAction; 27 | document.body.style['-webkit-overflow-scrolling'] = 28 | previousOverflowScrolling; 29 | document.body.style.overscrollBehavior = previousOverscrollBehavior; 30 | }; 31 | }, []); 32 | 33 | return null; 34 | }; 35 | 36 | export default useDisableScroll; 37 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, useRef } from 'react'; 2 | import CSSTransition from 'react-transition-group/CSSTransition'; 3 | 4 | import ReactPortal from './components/ReactPortal'; 5 | 6 | import DisableScroll from './hooks/useDisableScroll'; 7 | 8 | import { CloseIcon } from './assets/icons'; 9 | 10 | import styles from './styles.scss'; 11 | 12 | import { ModalProps, AnimationType } from './types'; 13 | 14 | const Modal = ({ 15 | children, 16 | titleElement, 17 | closeIcon, 18 | isOpen, 19 | onClose, 20 | enterAnimation = 'zoom-in', 21 | exitAnimation = 'zoom-in', 22 | classNames = {}, 23 | timeout = 250, 24 | }: ModalProps) => { 25 | const modalRef = useRef(null); 26 | 27 | const keyDownHandler: React.KeyboardEventHandler = (e) => { 28 | if (e.key === 'Escape') onClose(); 29 | }; 30 | 31 | const { 32 | root = '', 33 | backdrop = '', 34 | content = '', 35 | header = '', 36 | body = '', 37 | closeButton = '', 38 | } = classNames; 39 | 40 | return ( 41 | 42 | modalRef.current?.focus()} 54 | > 55 | <> 56 | 57 |
64 |
69 | 70 |
73 |
76 | {titleElement} 77 | 85 |
86 | 87 |
88 | {children} 89 |
90 |
91 |
92 | 93 |
94 |
95 | ); 96 | }; 97 | 98 | export default Modal; 99 | export { AnimationType, DisableScroll as useDisableScroll }; 100 | export { ANIMATIONS } from './config'; 101 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: fixed; 3 | inset: 0; 4 | padding: 1rem; 5 | display: grid; 6 | place-items: center; 7 | perspective: 1000px; 8 | z-index: 99999; 9 | 10 | &.enter { 11 | > .backdrop { 12 | opacity: 0; 13 | } 14 | } 15 | 16 | &.enterActive { 17 | > .backdrop { 18 | opacity: 1; 19 | transition: opacity calc(var(--timeout) * 1ms) ease; 20 | } 21 | } 22 | 23 | &.exit { 24 | > .backdrop { 25 | opacity: 1; 26 | } 27 | } 28 | 29 | &.exitActive { 30 | > .backdrop { 31 | opacity: 0; 32 | transition: opacity calc(var(--timeout) * 1ms) ease; 33 | } 34 | } 35 | 36 | // CONTENT TRANSITIONS 37 | 38 | &.fade { 39 | &.enter { 40 | > .content { 41 | opacity: 0; 42 | } 43 | } 44 | 45 | &.enterActive { 46 | > .content { 47 | opacity: 1; 48 | transition: opacity calc(calc(var(--timeout) * 1ms) * 0.8) 49 | calc(calc(var(--timeout) * 1ms) * 0.2) ease; 50 | } 51 | } 52 | 53 | &.exit { 54 | > .content { 55 | opacity: 1; 56 | } 57 | } 58 | 59 | &.exitActive { 60 | > .content { 61 | opacity: 0; 62 | transition: opacity calc(calc(var(--timeout) * 1ms) * 0.8) 63 | calc(calc(var(--timeout) * 1ms) * 0.2) ease; 64 | } 65 | } 66 | } 67 | 68 | &.fade-left { 69 | &.enter { 70 | > .content { 71 | transform: translateX(-10vw); 72 | opacity: 0; 73 | } 74 | } 75 | 76 | &.enterActive { 77 | > .content { 78 | transform: translate(0); 79 | opacity: 1; 80 | transition: transform calc(var(--timeout) * 1ms) ease, 81 | opacity calc(var(--timeout) * 1ms) ease; 82 | } 83 | } 84 | 85 | &.exit { 86 | > .content { 87 | transform: translate(0); 88 | opacity: 1; 89 | } 90 | } 91 | 92 | &.exitActive { 93 | > .content { 94 | transform: translateX(-10vw); 95 | opacity: 0; 96 | transition: transform calc(var(--timeout) * 1ms) ease, 97 | opacity calc(var(--timeout) * 1ms) ease; 98 | } 99 | } 100 | } 101 | 102 | &.fade-right { 103 | &.enter { 104 | > .content { 105 | transform: translateX(10vh); 106 | opacity: 0; 107 | } 108 | } 109 | 110 | &.enterActive { 111 | > .content { 112 | transform: translate(0); 113 | opacity: 1; 114 | transition: transform calc(var(--timeout) * 1ms) ease, 115 | opacity calc(var(--timeout) * 1ms) ease; 116 | } 117 | } 118 | 119 | &.exit { 120 | > .content { 121 | transform: translate(0); 122 | opacity: 1; 123 | } 124 | } 125 | 126 | &.exitActive { 127 | > .content { 128 | transform: translateX(10vw); 129 | opacity: 0; 130 | transition: transform calc(var(--timeout) * 1ms) ease, 131 | opacity calc(var(--timeout) * 1ms) ease; 132 | } 133 | } 134 | } 135 | 136 | &.fade-top { 137 | &.enter { 138 | > .content { 139 | transform: translateY(-10vh); 140 | opacity: 0; 141 | } 142 | } 143 | 144 | &.enterActive { 145 | > .content { 146 | transform: translate(0); 147 | opacity: 1; 148 | transition: transform calc(var(--timeout) * 1ms) ease, 149 | opacity calc(var(--timeout) * 1ms) ease; 150 | } 151 | } 152 | 153 | &.exit { 154 | > .content { 155 | transform: translate(0); 156 | opacity: 1; 157 | } 158 | } 159 | 160 | &.exitActive { 161 | > .content { 162 | transform: translateY(-10vh); 163 | opacity: 0; 164 | transition: transform calc(var(--timeout) * 1ms) ease, 165 | opacity calc(var(--timeout) * 1ms) ease; 166 | } 167 | } 168 | } 169 | 170 | &.fade-bottom { 171 | &.enter { 172 | > .content { 173 | transform: translateY(10vh); 174 | opacity: 0; 175 | } 176 | } 177 | 178 | &.enterActive { 179 | > .content { 180 | transform: translate(0); 181 | opacity: 1; 182 | transition: transform calc(var(--timeout) * 1ms) ease, 183 | opacity calc(var(--timeout) * 1ms) ease; 184 | } 185 | } 186 | 187 | &.exit { 188 | > .content { 189 | transform: translate(0); 190 | opacity: 1; 191 | } 192 | } 193 | 194 | &.exitActive { 195 | > .content { 196 | transform: translateY(10vh); 197 | opacity: 0; 198 | transition: transform calc(var(--timeout) * 1ms) ease, 199 | opacity calc(var(--timeout) * 1ms) ease; 200 | } 201 | } 202 | } 203 | 204 | &.slide-left { 205 | &.enter { 206 | > .content { 207 | transform: translateX(-100vw); 208 | } 209 | } 210 | 211 | &.enterActive { 212 | > .content { 213 | transform: translate(0); 214 | transition: transform calc(var(--timeout) * 1ms) ease; 215 | } 216 | } 217 | 218 | &.exit { 219 | > .content { 220 | transform: translate(0); 221 | } 222 | } 223 | 224 | &.exitActive { 225 | > .content { 226 | transform: translateX(-100vw); 227 | transition: transform calc(var(--timeout) * 1ms) ease; 228 | } 229 | } 230 | } 231 | 232 | &.slide-right { 233 | &.enter { 234 | > .content { 235 | transform: translateX(100vw); 236 | } 237 | } 238 | 239 | &.enterActive { 240 | > .content { 241 | transform: translate(0); 242 | transition: transform calc(var(--timeout) * 1ms) ease; 243 | } 244 | } 245 | 246 | &.exit { 247 | > .content { 248 | transform: translate(0); 249 | } 250 | } 251 | 252 | &.exitActive { 253 | > .content { 254 | transform: translateX(100vw); 255 | transition: transform calc(var(--timeout) * 1ms) ease; 256 | } 257 | } 258 | } 259 | 260 | &.slide-top { 261 | &.enter { 262 | > .content { 263 | transform: translateY(-100vw); 264 | } 265 | } 266 | 267 | &.enterActive { 268 | > .content { 269 | transform: translate(0); 270 | transition: transform calc(var(--timeout) * 1ms) ease; 271 | } 272 | } 273 | 274 | &.exit { 275 | > .content { 276 | transform: translate(0); 277 | } 278 | } 279 | 280 | &.exitActive { 281 | > .content { 282 | transform: translateY(-100vw); 283 | transition: transform calc(var(--timeout) * 1ms) ease; 284 | } 285 | } 286 | } 287 | 288 | &.slide-bottom { 289 | &.enter { 290 | > .content { 291 | transform: translateY(100vw); 292 | } 293 | } 294 | 295 | &.enterActive { 296 | > .content { 297 | transform: translate(0); 298 | transition: transform calc(var(--timeout) * 1ms) ease; 299 | } 300 | } 301 | 302 | &.exit { 303 | > .content { 304 | transform: translate(0); 305 | } 306 | } 307 | 308 | &.exitActive { 309 | > .content { 310 | transform: translateY(100vw); 311 | transition: transform calc(var(--timeout) * 1ms) ease; 312 | } 313 | } 314 | } 315 | 316 | &.spin-cw { 317 | &.enter { 318 | > .content { 319 | transform: rotate(-120deg) scale(0.5); 320 | opacity: 0; 321 | } 322 | } 323 | 324 | &.enterActive { 325 | > .content { 326 | transform: rotate(0) scale(1); 327 | opacity: 1; 328 | 329 | transition: transform calc(var(--timeout) * 1ms) ease, 330 | opacity calc(var(--timeout) * 1ms) ease; 331 | } 332 | } 333 | 334 | &.exit { 335 | > .content { 336 | transform: rotate(0) scale(1); 337 | opacity: 1; 338 | } 339 | } 340 | 341 | &.exitActive { 342 | > .content { 343 | transform: rotate(-120deg) scale(0.5); 344 | opacity: 0; 345 | transition: transform calc(var(--timeout) * 1ms) ease, 346 | opacity calc(var(--timeout) * 1ms) ease; 347 | } 348 | } 349 | } 350 | 351 | &.spin-ccw { 352 | &.enter { 353 | > .content { 354 | transform: rotate(120deg) scale(0.5); 355 | opacity: 0; 356 | } 357 | } 358 | 359 | &.enterActive { 360 | > .content { 361 | transform: rotate(0) scale(1); 362 | opacity: 1; 363 | 364 | transition: transform calc(var(--timeout) * 1ms) ease, 365 | opacity calc(var(--timeout) * 1ms) ease; 366 | } 367 | } 368 | 369 | &.exit { 370 | > .content { 371 | transform: rotate(0) scale(1); 372 | opacity: 1; 373 | } 374 | } 375 | 376 | &.exitActive { 377 | > .content { 378 | transform: rotate(120deg) scale(0.5); 379 | opacity: 0; 380 | transition: transform calc(var(--timeout) * 1ms) ease, 381 | opacity calc(var(--timeout) * 1ms) ease; 382 | } 383 | } 384 | } 385 | 386 | &.rotate-left { 387 | &.enter { 388 | > .content { 389 | transform: rotateY(100deg); 390 | transform-origin: calc(50% - 50vw) center; 391 | opacity: 0; 392 | } 393 | } 394 | 395 | &.enterActive { 396 | > .content { 397 | transform: rotateY(0deg); 398 | opacity: 1; 399 | transition: transform calc(var(--timeout) * 1ms) ease, 400 | opacity calc(var(--timeout) * 1ms) ease; 401 | } 402 | } 403 | 404 | &.exit { 405 | > .content { 406 | transform: rotateY(0deg); 407 | transform-origin: calc(50% - 50vw) center; 408 | opacity: 1; 409 | } 410 | } 411 | 412 | &.exitActive { 413 | > .content { 414 | transform: rotateY(100deg); 415 | opacity: 0; 416 | transition: transform calc(var(--timeout) * 1ms) ease, 417 | opacity calc(var(--timeout) * 1ms) ease; 418 | } 419 | } 420 | } 421 | 422 | &.rotate-right { 423 | &.enter { 424 | > .content { 425 | transform: rotateY(-100deg); 426 | transform-origin: calc(50% + 50vw) center; 427 | opacity: 0; 428 | } 429 | } 430 | 431 | &.enterActive { 432 | > .content { 433 | transform: rotateY(0deg); 434 | opacity: 1; 435 | transition: transform calc(var(--timeout) * 1ms) ease, 436 | opacity calc(var(--timeout) * 1ms) ease; 437 | } 438 | } 439 | 440 | &.exit { 441 | > .content { 442 | transform: rotateY(0deg); 443 | transform-origin: calc(50% + 50vw) center; 444 | opacity: 1; 445 | } 446 | } 447 | 448 | &.exitActive { 449 | > .content { 450 | transform: rotateY(-100deg); 451 | opacity: 0; 452 | transition: transform calc(var(--timeout) * 1ms) ease, 453 | opacity calc(var(--timeout) * 1ms) ease; 454 | } 455 | } 456 | } 457 | 458 | &.rotate-top { 459 | &.enter { 460 | > .content { 461 | transform: rotateX(-100deg); 462 | transform-origin: center calc(50% - 50vh); 463 | opacity: 0; 464 | } 465 | } 466 | 467 | &.enterActive { 468 | > .content { 469 | transform: rotateX(0deg); 470 | opacity: 1; 471 | transition: transform calc(var(--timeout) * 1ms) ease, 472 | opacity calc(var(--timeout) * 1ms) ease; 473 | } 474 | } 475 | 476 | &.exit { 477 | > .content { 478 | transform: rotateX(0deg); 479 | transform-origin: center calc(50% - 50vh); 480 | opacity: 1; 481 | } 482 | } 483 | 484 | &.exitActive { 485 | > .content { 486 | transform: rotateX(-100deg); 487 | opacity: 0; 488 | transition: transform calc(var(--timeout) * 1ms) ease, 489 | opacity calc(var(--timeout) * 1ms) ease; 490 | } 491 | } 492 | } 493 | 494 | &.rotate-bottom { 495 | &.enter { 496 | > .content { 497 | transform: rotateX(100deg); 498 | transform-origin: center calc(50% + 50vh); 499 | opacity: 0; 500 | } 501 | } 502 | 503 | &.enterActive { 504 | > .content { 505 | transform: rotateX(0deg); 506 | opacity: 1; 507 | transition: transform calc(var(--timeout) * 1ms) ease, 508 | opacity calc(var(--timeout) * 1ms) ease; 509 | } 510 | } 511 | 512 | &.exit { 513 | > .content { 514 | transform: rotateX(0deg); 515 | transform-origin: center calc(50% + 50vh); 516 | opacity: 1; 517 | } 518 | } 519 | 520 | &.exitActive { 521 | > .content { 522 | transform: rotateX(100deg); 523 | opacity: 0; 524 | transition: transform calc(var(--timeout) * 1ms) ease, 525 | opacity calc(var(--timeout) * 1ms) ease; 526 | } 527 | } 528 | } 529 | 530 | &.zoom-in { 531 | &.enter { 532 | > .content { 533 | transform: scale(0.5); 534 | opacity: 0; 535 | } 536 | } 537 | 538 | &.enterActive { 539 | > .content { 540 | transform: scale(1); 541 | opacity: 1; 542 | transition: transform calc(var(--timeout) * 1ms) ease, 543 | opacity calc(var(--timeout) * 1ms) ease; 544 | } 545 | } 546 | 547 | &.exit { 548 | > .content { 549 | transform: scale(1); 550 | opacity: 1; 551 | } 552 | } 553 | 554 | &.exitActive { 555 | > .content { 556 | transform: scale(0.5); 557 | opacity: 0; 558 | transition: transform calc(var(--timeout) * 1ms) ease, 559 | opacity calc(var(--timeout) * 1ms) ease; 560 | } 561 | } 562 | } 563 | 564 | &.zoom-out { 565 | &.enter { 566 | > .content { 567 | transform: scale(1.2); 568 | opacity: 0; 569 | } 570 | } 571 | 572 | &.enterActive { 573 | > .content { 574 | transform: scale(1); 575 | opacity: 1; 576 | transition: transform calc(var(--timeout) * 1ms) ease, 577 | opacity calc(var(--timeout) * 1ms) ease; 578 | } 579 | } 580 | 581 | &.exit { 582 | > .content { 583 | transform: scale(1); 584 | opacity: 1; 585 | } 586 | } 587 | 588 | &.exitActive { 589 | > .content { 590 | transform: scale(1.2); 591 | opacity: 0; 592 | transition: transform calc(var(--timeout) * 1ms) ease, 593 | opacity calc(var(--timeout) * 1ms) ease; 594 | } 595 | } 596 | } 597 | } 598 | 599 | .backdrop { 600 | position: fixed; 601 | inset: 0; 602 | width: 100%; 603 | height: 100%; 604 | background-color: rgba(0, 0, 0, 0.6); 605 | z-index: -1; 606 | } 607 | 608 | .content { 609 | background-color: #ffffff; 610 | border-radius: 0.5rem; 611 | overflow: hidden; 612 | max-height: 100%; 613 | min-height: 0; 614 | width: 100%; 615 | max-width: 720px; 616 | display: flex; 617 | flex-direction: column; 618 | } 619 | 620 | .header { 621 | display: flex; 622 | padding: 0.75rem 1rem; 623 | background-color: #d1d5db; 624 | border-radius: 0.25rem; 625 | } 626 | 627 | .closeButton { 628 | margin-left: auto; 629 | font-size: 24px; 630 | 631 | > * { 632 | display: block; 633 | } 634 | } 635 | 636 | .body { 637 | flex: 1; 638 | min-height: 0; 639 | overflow-y: auto; 640 | overscroll-behavior-y: contain; 641 | padding: 1rem 1.25rem; 642 | 643 | /* 644 | You can either use overscroll-behavior: none; to disable scroll or the useDisableScroll() hook. 645 | The downside of this css method is that the scroll for the main body 646 | is still visible even if the modal is open and its not supported on some browsers. 647 | */ 648 | // overscroll-behavior: none; 649 | } 650 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ANIMATIONS } from './config'; 2 | 3 | export type AnimationType = typeof ANIMATIONS[number]; 4 | 5 | export type ModalProps = { 6 | titleElement?: React.ReactNode; 7 | closeIcon?: React.ReactNode; 8 | children: React.ReactNode; 9 | isOpen: boolean; 10 | enterAnimation?: AnimationType; 11 | exitAnimation?: AnimationType; 12 | onClose: () => void; 13 | classNames?: { 14 | root?: string; 15 | backdrop?: string; 16 | content?: string; 17 | header?: string; 18 | closeButton?: string; 19 | body?: string; 20 | }; 21 | timeout?: number; 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "lib": [ 5 | "ES6", 6 | "DOM", 7 | "ES2016", 8 | "ES2017" 9 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 10 | "jsx": "react" /* Specify what JSX code is generated. */, 11 | "module": "esnext" /* Specify what module code is generated. */, 12 | "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, 13 | "allowJs": false /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 14 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 15 | "sourceMap": true /* Create source map files for emitted JavaScript files. */, 16 | "outDir": "dist" /* Specify an output folder for all emitted files. */, 17 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 18 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 19 | "strict": true /* Enable all strict type-checking options. */, 20 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */, 23 | "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */, 24 | "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */, 25 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read. */, 26 | "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, 27 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 28 | }, 29 | "files": ["types.d.ts"], 30 | "include": ["src"], 31 | "exclude": ["node_modules", "dist", "example", "rollup.config.js"] 32 | } 33 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React from 'react'; 3 | const SVG: React.FC>; 4 | export default SVG; 5 | } 6 | 7 | declare module '*.scss' { 8 | const classes: { readonly [key: string]: string }; 9 | export default classes; 10 | } 11 | --------------------------------------------------------------------------------