Modal
기존 흐름을 방해하지 않고 새로운 상호작용을 하고자 할 때 사용합니다.
Example
It can only be viewed in a mobile.
Steps
Prerequisite
Install Package
npm install lucide-react
Add types
types/index.d.ts
interface ReactProps {
children?: ReactNode
}
interface ModalProps {
isOpen: boolean
onClose: () => void
title?: string
/**
* @default "max-w-lg"
*/
maxWidth?:
| 'max-w-screen-2xl'
| 'max-w-screen-xl'
| 'max-w-screen-lg'
| 'max-w-screen-md'
| 'max-w-screen-sm'
| 'max-w-full'
| 'max-w-7xl'
| 'max-w-6xl'
| 'max-w-5xl'
| 'max-w-4xl'
| 'max-w-3xl'
| 'max-w-2xl'
| 'max-w-xl'
| 'max-w-lg'
| 'max-w-md'
| 'max-w-sm'
| 'max-w-xs'
description?: ReactNode
padding?: boolean
footer?: ReactNode
}
Copy Code
containers/Modal/index.tsx
'use client'
import { useCallback, useEffect } from 'react'
import { XIcon } from 'lucide-react'
import { createPortal } from 'react-dom'
import { cn } from 'utils'
export interface Props extends ReactProps, ModalProps {}
function Modal({
isOpen,
onClose,
children,
maxWidth = 'max-w-lg',
title,
description,
padding = true,
footer
}: Props) {
const onEscape = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose()
window.removeEventListener('keydown', onEscape)
}
},
[onClose]
)
useEffect(() => {
if (!isOpen) return
window.addEventListener('keydown', onEscape)
return () => {
window.removeEventListener('keydown', onEscape)
}
}, [isOpen, onEscape])
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else if (document.body.style.overflow === 'hidden') {
document.body.style.removeProperty('overflow')
}
}, [isOpen])
if (!isOpen) return <></>
return createPortal(
<div
className="fixed inset-0 z-30 overflow-y-auto"
aria-labelledby="modal-title"
aria-modal="true"
role="dialog"
>
<div className="flex min-h-screen items-center justify-center p-0 text-center md:block">
<div
className="fixed inset-0 bg-black/30 backdrop-blur-sm transition-opacity"
aria-hidden="true"
onClick={onClose}
/>
<span
className="hidden h-screen align-middle md:inline-block"
aria-hidden="true"
>
​
</span>
<div
className={cn(
'my-8 inline-block w-full transform overflow-hidden rounded-lg text-left align-middle shadow-xl transition-all',
maxWidth
)}
>
{title && (
<header className="border-t-4 border-blue-500 bg-white dark:bg-neutral-800">
<div className="flex items-center border-b border-neutral-200 px-4 py-3 dark:border-neutral-700">
<div className="flex-1">
<h1 className="text-xl font-semibold">{title}</h1>
{!!description && (
<p className="mt-1 text-sm text-neutral-400">
{description}
</p>
)}
</div>
<button
onClick={onClose}
className="rounded-full p-2 hover:bg-neutral-200 dark:hover:bg-neutral-900"
>
<XIcon className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />
</button>
</div>
</header>
)}
<div
className={cn('bg-white dark:bg-neutral-900', {
'px-7 py-6': padding,
'rounded-b-lg': !footer
})}
>
{children}
</div>
{footer && (
<footer className="rounded-b-lg border-t bg-white px-7 py-4 dark:border-neutral-700 dark:bg-neutral-900">
{footer}
</footer>
)}
</div>
</div>
</div>,
document.body
)
}
export default Modal
Usage
<Modal
isOpen={isOpen}
onClose={() => setState({ isOpen: false })}
padding
footer={<div>Footer</div>}
maxWidth="max-w-lg"
title="Title"
description={<p>description</p>}
>
children
</Modal>
Props
Name | Type | Default |
---|---|---|
title | string | |
onClose | boolean | |
onClose | () => void | |
description | string | |
padding | boolean | |
footer | ReactNode | |
children | ReactNode | |
maxWidth | max-w-screen-2xl max-w-screen-xl max-w-screen-lg max-w-screen-md max-w-screen-sm max-w-full max-w-8xl max-w-7xl max-w-6xl max-w-5xl max-w-4xl max-w-3xl max-w-2xl max-w-xl max-w-lg max-w-md max-w-sm max-w-xs |