// Accessible Modal.tsx with // Framer Motion and Tailwind CSS // 10 Examples in the comments section below import { createPortal } from 'react-dom' import { Dispatch, HTMLAttributes, SetStateAction, useEffect, useState } from 'react' import { AnimatePresence, motion } from 'framer-motion' import FocusLock from 'react-focus-lock' const effect = { hidden: { y: '-100vh', opacity: 0, }, visible: { y: '0', opacity: 1, transition: { type: 'spring', stiffness: 600, damping: 30, }, }, exit: { y: '100vh', opacity: 0, }, } const Backdrop = ({ children, handleClose }: BackdropProps) => ( {children} ) const ModalContent = ({ className, children, handleClose, ariaLabel }: ModalContentProps) => ( event.stopPropagation()} > {children} {handleClose && ( )} ) export const Modal = ({ children, className, isOpen, handleClose, hideCloseButton, backdropDismiss = true, onExitComplete, ariaLabel, }: ModalProps) => { const [isBrowser, setIsBrowser] = useState(false) const [trigger, setTrigger] = onExitComplete ?? [undefined, undefined] const handleKeyDown = (event: KeyboardEvent) => { if (!isOpen || event.key !== 'Escape') return handleClose() } useEffect(() => { if (!isOpen) return document.body.style.overflow = 'hidden' document.addEventListener('keydown', handleKeyDown) return () => { document.body.style.overflow = 'auto' document.removeEventListener('keydown', handleKeyDown) } }, [isOpen]) useEffect(() => { setIsBrowser(true) }, []) if (!isBrowser) return <> return createPortal( setTrigger && trigger === 'fired' && setTrigger('completed')} > {isOpen && ( {children} )} , document.getElementById('modal-portal')! ) } type ModalProps = HTMLAttributes & { isOpen: boolean handleClose: () => void hideCloseButton?: boolean backdropDismiss?: boolean onExitComplete?: [string, Dispatch>] ariaLabel?: string } type ModalContentProps = HTMLAttributes & { handleClose?: () => void ariaLabel?: string } type BackdropProps = HTMLAttributes & { handleClose?: () => void }