ComponentsBasic Modal

Basic Modal

A polished modal dialog with backdrop blur and dynamic entrance/exit animations that improves user experience for dialog interactions.

Code

Install with shadcn Beta

Terminal

npx shadcn@latest add "https://smoothui.dev/r/basic-modal.json"

Manual install

Terminal

npm install motion lucide-react usehooks-ts

BasicModal.tsx

"use client"

import { useEffect, useRef, useState } from "react"
import { X } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { useOnClickOutside } from "usehooks-ts"

interface BasicModalProps {
  isOpen: boolean
  onClose: () => void
  title?: string
  children: React.ReactNode
  size?: "sm" | "md" | "lg" | "xl" | "full"
}

const modalSizes = {
  sm: "max-w-sm",
  md: "max-w-md",
  lg: "max-w-lg",
  xl: "max-w-xl",
  full: "max-w-4xl",
}

export default function BasicModal({
  isOpen,
  onClose,
  title,
  children,
  size = "md",
}: BasicModalProps) {
  const overlayRef = useRef<HTMLDivElement>(null)
  const modalRef = useRef<HTMLDivElement>(
    null
  ) as React.RefObject<HTMLDivElement>
  useOnClickOutside(modalRef, () => onClose())

  // Close on Escape key press
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape" && isOpen) {
        onClose()
      }
    }

    document.addEventListener("keydown", handleKeyDown)
    return () => document.removeEventListener("keydown", handleKeyDown)
  }, [isOpen, onClose])

  // Prevent body scroll when modal is open
  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = "hidden"
    } else {
      document.body.style.overflow = ""
    }
    return () => {
      document.body.style.overflow = ""
    }
  }, [isOpen])

  return (
    <AnimatePresence>
      {isOpen && (
        <>
          {/* Backdrop */}
          <motion.div
            ref={overlayRef}
            className="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.2 }}
            onClick={(e) => {
              if (e.target === overlayRef.current) {
                onClose()
              }
            }}
          />

          {/* Modal */}
          <motion.div
            className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto px-4 py-6 sm:p-0"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            <motion.div
              ref={modalRef}
              className={`${modalSizes[size]} border-light-300 bg-light-100 dark:border-dark-300 dark:bg-dark-100 relative mx-auto w-full rounded-xl border p-4 shadow-xl sm:p-6`}
              initial={{ scale: 0.9, y: 20, opacity: 0 }}
              animate={{ scale: 1, y: 0, opacity: 1 }}
              exit={{
                scale: 0.95,
                y: 10,
                opacity: 0,
                transition: { duration: 0.15 },
              }}
              transition={{ type: "spring", damping: 25, stiffness: 300 }}
            >
              {/* Header */}
              <div className="mb-4 flex items-center justify-between">
                {title && (
                  <h3 className="text-xl leading-6 font-medium">{title}</h3>
                )}
                <motion.button
                  className="hover:bg-light-200 dark:hover:bg-dark-200 ml-auto rounded-full p-1.5 transition-colors"
                  onClick={onClose}
                  whileHover={{ rotate: 90 }}
                  transition={{ duration: 0.2 }}
                >
                  <X className="h-5 w-5" />
                  <span className="sr-only">Close</span>
                </motion.button>
              </div>

              {/* Content */}
              <div className="relative">{children}</div>
            </motion.div>
          </motion.div>
        </>
      )}
    </AnimatePresence>
  )
}

export function ModalDemo() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div className="flex flex-col gap-4 p-8">
      <button
        onClick={() => setIsOpen(true)}
        className="border-light-200 bg-light-50 dark:border-dark-200 dark:bg-dark-50 cursor-pointer rounded-md border p-3 shadow-xs"
      >
        Open Modal
      </button>

      <BasicModal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        title="Beautiful Modal"
        size="md"
      >
        <div className="flex flex-col gap-4">
          <p>
            This is a beautiful animated modal with smooth entrance and exit
            animations. Click outside or press Escape to close.
          </p>

          <div className="flex flex-col gap-2">
            <h4 className="font-medium">Features:</h4>
            <ul className="list-inside list-disc space-y-1">
              <li>Smooth animations</li>
              <li>Backdrop blur effect</li>
              <li>Responsive design</li>
              <li>Keyboard navigation (ESC to close)</li>
              <li>Focus trapping (for accessibility)</li>
            </ul>
          </div>

          <div className="mt-4 flex justify-end gap-2">
            <button
              onClick={() => setIsOpen(false)}
              className="border-light-300 hover:bg-light-200 dark:border-dark-300 dark:hover:bg-dark-200 rounded-lg border px-4 py-2 transition-colors"
            >
              Cancel
            </button>
            <button
              onClick={() => setIsOpen(false)}
              className="rounded-lg bg-pink-500 px-4 py-2 text-white transition-colors hover:bg-pink-600"
            >
              Confirm
            </button>
          </div>
        </div>
      </BasicModal>
    </div>
  )
}