// 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
}