Skip to content

Instantly share code, notes, and snippets.

@magalhaespaulo
Last active June 1, 2023 21:46
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save magalhaespaulo/737a5c35048c18b8a2209d8a9fae977c to your computer and use it in GitHub Desktop.
Save magalhaespaulo/737a5c35048c18b8a2209d8a9fae977c to your computer and use it in GitHub Desktop.
Accessible <Modal /> with Framer Motion and Tailwind CSS
// 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) => (
<motion.div
className="
z-50 fixed inset-0
flex items-center justify-center
bg-backdrop backdrop-filter backdrop-blur-sm
"
onClick={handleClose}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{children}
</motion.div>
)
const ModalContent = ({ className, children, handleClose, ariaLabel }: ModalContentProps) => (
<motion.div
tabIndex={-1}
role="dialog"
aria-modal={true}
aria-label={ariaLabel}
className={`relative ${className || 'm-5 p-5 bg-white rounded-lg shadow-lg'}`}
variants={effect}
initial="hidden"
animate="visible"
exit="exit"
onClick={(event) => event.stopPropagation()}
>
{children}
{handleClose && (
<button
className="absolute top-0 right-0 p-2"
onClick={handleClose}
aria-label={`Close ${ariaLabel || 'dialog'}`}
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="M289.94 256l95-95A24 24 0 00351 127l-95 95-95-95a24 24 0 00-34 34l95 95-95 95a24 24 0 1034 34l95-95 95 95a24 24 0 0034-34z" />
</svg>
</button>
)}
</motion.div>
)
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(
<AnimatePresence
initial={false}
exitBeforeEnter={true}
onExitComplete={() => setTrigger && trigger === 'fired' && setTrigger('completed')}
>
{isOpen && (
<Backdrop handleClose={backdropDismiss ? handleClose : undefined}>
<FocusLock>
<ModalContent
className={className}
handleClose={hideCloseButton ? undefined : handleClose}
ariaLabel={ariaLabel}
>
{children}
</ModalContent>
</FocusLock>
</Backdrop>
)}
</AnimatePresence>,
document.getElementById('modal-portal')!
)
}
type ModalProps = HTMLAttributes<HTMLDivElement> & {
isOpen: boolean
handleClose: () => void
hideCloseButton?: boolean
backdropDismiss?: boolean
onExitComplete?: [string, Dispatch<SetStateAction<string>>]
ariaLabel?: string
}
type ModalContentProps = HTMLAttributes<HTMLDivElement> & {
handleClose?: () => void
ariaLabel?: string
}
type BackdropProps = HTMLAttributes<HTMLDivElement> & {
handleClose?: () => void
}
@magalhaespaulo
Copy link
Author

magalhaespaulo commented Nov 6, 2021

cover

See 10 examples here 😜


Define colors in tailwind.config.js

theme: {
  colors: {
    backdrop: 'rgba(0, 0, 0, 0.4)',
    white: '#ffffff',
  },
},
  1. All WAI-ARIA guidelines were followed.
  2. Is ready for Tailwind CSS ❤️ ...but you can switch easily if you modify the classNames in follow lines: 31, 51 and 61 in Modal.txs
  3. Don't forget to put the <div id="modal-portal"></div> as sibling with #root element

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment