Components
Modal
Modal
Ultra-modern modal component with backdrop blur, smooth animations, and customizable positioning for dialogs and overlays.
Modal Title
This is the modal content. You can put any content here.
Installation
1
Install the packages
npm i motion clsx tailwind-merge lucide-react2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
modal.tsx
"use client";
import { motion, AnimatePresence } from "motion/react";
import React, { useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
interface ModalProps {
isOpen: boolean;
onClose?: () => void;
title?: string;
description?: string;
children?: React.ReactNode;
size?: "sm" | "md" | "lg" | "xl" | "full";
variant?: "default" | "glass" | "blur" | "minimal";
position?: "center" | "top" | "bottom";
showCloseButton?: boolean;
closeOnBackdropClick?: boolean;
closeOnEscape?: boolean;
preventScroll?: boolean;
footer?: React.ReactNode;
className?: string;
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
description,
children,
size = "md",
variant = "default",
position = "center",
showCloseButton = true,
closeOnBackdropClick = true,
closeOnEscape = true,
preventScroll = true,
footer,
className,
}) => {
// Handle escape key
const handleEscape = useCallback((event: KeyboardEvent) => {
if (event.key === "Escape" && closeOnEscape && onClose) {
onClose();
}
}, [closeOnEscape, onClose]);
// Handle body scroll prevention
useEffect(() => {
if (isOpen && preventScroll) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen, preventScroll]);
// Handle escape key listener
useEffect(() => {
if (isOpen) {
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}
}, [isOpen, handleEscape]);
// Size classes
const sizeClasses = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
full: "max-w-4xl",
};
// Variant classes
const variantClasses = {
default: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700",
glass: "bg-white/90 dark:bg-gray-800/90 backdrop-blur-xl border border-white/20 dark:border-gray-700/50",
blur: "bg-white/80 dark:bg-gray-900/80 backdrop-blur-2xl border border-white/30 dark:border-gray-600/50",
minimal: "bg-white dark:bg-gray-800 border-0 shadow-2xl",
};
// Position classes
const positionClasses = {
center: "items-center",
top: "items-start pt-20",
bottom: "items-end pb-20",
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
className="fixed inset-0 z-40 flex justify-center p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{/* Backdrop Overlay */}
<motion.div
className={cn(
"absolute inset-0",
variant === "blur" ? "bg-black/20 backdrop-blur-sm" :
variant === "glass" ? "bg-black/10 backdrop-blur-md" :
"bg-black/50"
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={closeOnBackdropClick ? onClose : undefined}
transition={{ duration: 0.2 }}
/>
{/* Modal Content */}
<motion.div
className={cn(
"relative z-50 w-full rounded-xl shadow-2xl overflow-hidden",
sizeClasses[size],
variantClasses[variant],
positionClasses[position],
className
)}
initial={{
opacity: 0,
scale: 0.9,
y: position === "top" ? -20 : position === "bottom" ? 20 : 0
}}
animate={{
opacity: 1,
scale: 1,
y: 0
}}
exit={{
opacity: 0,
scale: 0.9,
y: position === "top" ? -20 : position === "bottom" ? 20 : 0
}}
transition={{
duration: 0.3,
ease: "easeOut"
}}
>
{/* Header */}
{(title || showCloseButton) && (
<motion.div
className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: 0.1 }}
>
<div className="flex-1">
{title && (
<motion.h2
className="text-lg font-semibold text-gray-900 dark:text-white"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: 0.2 }}
>
{title}
</motion.h2>
)}
{description && (
<motion.p
className="mt-1 text-sm text-gray-600 dark:text-gray-400"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: 0.3 }}
>
{description}
</motion.p>
)}
</div>
{showCloseButton && (
<motion.button
onClick={onClose}
className="flex-shrink-0 ml-4 p-1 rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2, delay: 0.4 }}
>
<X className="w-5 h-5" />
</motion.button>
)}
</motion.div>
)}
{/* Body */}
<motion.div
className="p-6"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: 0.2 }}
>
{children}
</motion.div>
{/* Footer */}
{footer && (
<motion.div
className="px-6 py-4 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-200 dark:border-gray-700"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: 0.3 }}
>
{footer}
</motion.div>
)}
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
);
};
export default Modal;"use client";
import { motion, AnimatePresence } from "motion/react";
import React, { useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
interface ModalProps {
isOpen: boolean;
onClose?: () => void;
title?: string;
description?: string;
children?: React.ReactNode;
size?: "sm" | "md" | "lg" | "xl" | "full";
variant?: "default" | "glass" | "blur" | "minimal";
position?: "center" | "top" | "bottom";
showCloseButton?: boolean;
closeOnBackdropClick?: boolean;
closeOnEscape?: boolean;
preventScroll?: boolean;
footer?: React.ReactNode;
className?: string;
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
description,
children,
size = "md",
variant = "default",
position = "center",
showCloseButton = true,
closeOnBackdropClick = true,
closeOnEscape = true,
preventScroll = true,
footer,
className,
}) => {
// Handle escape key
const handleEscape = useCallback((event: KeyboardEvent) => {
if (event.key === "Escape" && closeOnEscape && onClose) {
onClose();
}
}, [closeOnEscape, onClose]);
// Handle body scroll prevention
useEffect(() => {
if (isOpen && preventScroll) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen, preventScroll]);
// Handle escape key listener
useEffect(() => {
if (isOpen) {
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}
}, [isOpen, handleEscape]);
// Size classes
const sizeClasses = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
full: "max-w-4xl",
};
// Variant classes
const variantClasses = {
default: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700",
glass: "bg-white/90 dark:bg-gray-800/90 backdrop-blur-xl border border-white/20 dark:border-gray-700/50",
blur: "bg-white/80 dark:bg-gray-900/80 backdrop-blur-2xl border border-white/30 dark:border-gray-600/50",
minimal: "bg-white dark:bg-gray-800 border-0 shadow-2xl",
};
// Position classes
const positionClasses = {
center: "items-center",
top: "items-start pt-20",
bottom: "items-end pb-20",
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
className="fixed inset-0 z-40 flex justify-center p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{/* Backdrop Overlay */}
<motion.div
className={cn(
"absolute inset-0",
variant === "blur" ? "bg-black/20 backdrop-blur-sm" :
variant === "glass" ? "bg-black/10 backdrop-blur-md" :
"bg-black/50"
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={closeOnBackdropClick ? onClose : undefined}
transition={{ duration: 0.2 }}
/>
{/* Modal Content */}
<motion.div
className={cn(
"relative z-50 w-full rounded-xl shadow-2xl overflow-hidden",
sizeClasses[size],
variantClasses[variant],
positionClasses[position],
className
)}
initial={{
opacity: 0,
scale: 0.9,
y: position === "top" ? -20 : position === "bottom" ? 20 : 0
}}
animate={{
opacity: 1,
scale: 1,
y: 0
}}
exit={{
opacity: 0,
scale: 0.9,
y: position === "top" ? -20 : position === "bottom" ? 20 : 0
}}
transition={{
duration: 0.3,
ease: "easeOut"
}}
>
{/* Header */}
{(title || showCloseButton) && (
<motion.div
className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: 0.1 }}
>
<div className="flex-1">
{title && (
<motion.h2
className="text-lg font-semibold text-gray-900 dark:text-white"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: 0.2 }}
>
{title}
</motion.h2>
)}
{description && (
<motion.p
className="mt-1 text-sm text-gray-600 dark:text-gray-400"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: 0.3 }}
>
{description}
</motion.p>
)}
</div>
{showCloseButton && (
<motion.button
onClick={onClose}
className="flex-shrink-0 ml-4 p-1 rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2, delay: 0.4 }}
>
<X className="w-5 h-5" />
</motion.button>
)}
</motion.div>
)}
{/* Body */}
<motion.div
className="p-6"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: 0.2 }}
>
{children}
</motion.div>
{/* Footer */}
{footer && (
<motion.div
className="px-6 py-4 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-200 dark:border-gray-700"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: 0.3 }}
>
{footer}
</motion.div>
)}
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
);
};
export default Modal;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| isOpen | boolean | false | Controls the visibility of the modal. |
| onClose | () => void | undefined | Callback function when modal is closed. |
| title | string | undefined | Modal title text. |
| description | string | undefined | Modal description text. |
| children | ReactNode | undefined | Modal content. |
| size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | Modal size variant. |
| variant | 'default' | 'glass' | 'blur' | 'minimal' | 'default' | Modal visual variant. |
| position | 'center' | 'top' | 'bottom' | 'center' | Modal positioning. |
| showCloseButton | boolean | true | Show close button in header. |
| closeOnBackdropClick | boolean | true | Close modal when clicking backdrop. |
| closeOnEscape | boolean | true | Close modal on Escape key press. |
| preventScroll | boolean | true | Prevent body scroll when modal is open. |
| footer | ReactNode | undefined | Modal footer content. |
| className | string | undefined | Additional CSS classes. |