ComponentModalDialogOverlay

Modal Dialog

An accessible modal dialog component with backdrop, animations, and keyboard support. Uses React Portal for proper DOM placement.

Preview

Code

'use client'

import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { X } from 'lucide-react'
import { clsx } from 'clsx'

interface ModalProps {
  isOpen: boolean
  onClose: () => void
  title?: string
  children: React.ReactNode
}

export function Modal({ isOpen, onClose, title, children }: ModalProps) {
  const overlayRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose()
    }

    if (isOpen) {
      document.addEventListener('keydown', handleEscape)
      document.body.style.overflow = 'hidden'
    }

    return () => {
      document.removeEventListener('keydown', handleEscape)
      document.body.style.overflow = ''
    }
  }, [isOpen, onClose])

  if (!isOpen) return null

  return createPortal(
    <div
      ref={overlayRef}
      onClick={(e) => e.target === overlayRef.current && onClose()}
      className={clsx(
        'fixed inset-0 z-50 flex items-center justify-center p-4',
        'bg-black/50 backdrop-blur-sm',
        'animate-in fade-in duration-200'
      )}
    >
      <div className={clsx(
        'w-full max-w-md rounded-xl bg-white shadow-xl dark:bg-olive-900',
        'animate-in zoom-in-95 duration-200'
      )}>
        <div className="flex items-center justify-between border-b border-olive-100 px-6 py-4 dark:border-olive-800">
          {title && (
            <h2 className="font-display text-lg font-medium text-olive-950 dark:text-white">
              {title}
            </h2>
          )}
          <button
            onClick={onClose}
            className="rounded-lg p-1 text-olive-500 hover:bg-olive-100 dark:hover:bg-olive-800"
          >
            <X className="size-5" />
          </button>
        </div>
        <div className="px-6 py-4">{children}</div>
      </div>
    </div>,
    document.body
  )
}

Looking for more?

Browse the full collection of components or check out other exploration topics.