Unified Modal System
A single architectural primitive for all overlay interactions. Handles physics, trapping, and layout constraints automatically.
Modal is constrained to the preview canvas.
Live Canvas
Application Content Layer
Installation
This robust system handles all layout variants. Ensure you have `framer-motion` installed. Add this to `components/ui/modal.tsx`.
components/ui/modal.tsx
1"use client";23import React from "react";4import { X } from "lucide-react";5import { motion, AnimatePresence } from "framer-motion";6import { cn } from "@/lib/utils";78// --- Types ---9export type ModalPlacement = "center" | "top" | "bottom" | "left" | "right" | "fullscreen";10export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";1112interface ModalContextType {13 onClose: () => void;14}15const ModalContext = React.createContext<ModalContextType | undefined>(undefined);1617interface ModalProps {18 open: boolean;19 onOpenChange: (open: boolean) => void;20 children: React.ReactNode;21 placement?: ModalPlacement;22 size?: ModalSize;23 backdrop?: "blur" | "dark" | "none";24 className?: string;25}2627// --- Main Component ---28export const Modal = ({29 open,30 onOpenChange,31 children,32 placement = "center",33 size = "md",34 backdrop = "blur",35 className,36}: ModalProps) => {3738 const placementStyles = {39 center: "items-center justify-center p-4",40 top: "items-start justify-center p-4 pt-10",41 bottom: "items-end justify-center p-4 pb-0 md:pb-10",42 left: "items-center justify-start",43 right: "items-center justify-end",44 fullscreen: "items-center justify-center",45 };4647 const animationVariants = {48 center: { initial: { opacity: 0, scale: 0.95 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.95 } },49 top: { initial: { opacity: 0, y: -20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 } },50 bottom: { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 20 } },51 left: { initial: { opacity: 0, x: -20 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: -20 } },52 right: { initial: { opacity: 0, x: 20 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: 20 } },53 fullscreen: { initial: { opacity: 0, scale: 0.98 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.98 } },54 };5556 const sizeClasses = {57 sm: "max-w-sm",58 md: "max-w-md",59 lg: "max-w-2xl",60 xl: "max-w-4xl",61 full: "max-w-full h-full",62 };6364 const isSheet = placement === "left" || placement === "right";65 const isFullscreen = placement === "fullscreen";6667 return (68 <ModalContext.Provider value={{ onClose: () => onOpenChange(false) }}>69 <AnimatePresence>70 {open && (71 <div72 className={cn(73 "fixed inset-0 z-50 flex overflow-hidden",74 placementStyles[placement],75 className76 )}77 role="dialog"78 aria-modal="true"79 >80 {/* Backdrop */}81 {backdrop !== "none" && (82 <motion.div83 initial={{ opacity: 0 }}84 animate={{ opacity: 1 }}85 exit={{ opacity: 0 }}86 onClick={() => onOpenChange(false)}87 className={cn(88 "absolute inset-0 z-0",89 backdrop === "blur" ? "bg-black/40 backdrop-blur-sm" : "bg-black/60"90 )}91 />92 )}9394 {/* Content */}95 <motion.div96 variants={animationVariants[placement]}97 initial="initial"98 animate="animate"99 exit="exit"100 transition={{ type: "spring", stiffness: 350, damping: 25 }}101 className={cn(102 "relative z-10 flex flex-col bg-[#0F0F10] border border-white/10 shadow-2xl overflow-hidden",103 isSheet || isFullscreen ? "h-full rounded-none" : "rounded-xl max-h-[90%] w-full",104 !isFullscreen && !isSheet ? sizeClasses[size] : "",105 isSheet ? (size === "sm" ? "w-72" : "w-80 md:w-96") : "",106 isSheet && placement === "right" ? "border-l" : "",107 isSheet && placement === "left" ? "border-r" : "",108 isFullscreen ? "w-full h-full border-none" : ""109 )}110 >111 {children}112 </motion.div>113 </div>114 )}115 </AnimatePresence>116 </ModalContext.Provider>117 );118};119120// --- Subcomponents ---121122export const ModalHeader = ({ children, className }: { children: React.ReactNode; className?: string }) => {123 const ctx = React.useContext(ModalContext);124 return (125 <div className={cn("px-6 py-4 border-b border-white/5 flex items-center justify-between shrink-0 bg-[#0F0F10]", className)}>126 <div className="font-semibold text-white">{children}</div>127 <button onClick={ctx?.onClose} className="text-neutral-500 hover:text-white transition-colors">128 <X className="w-4 h-4" />129 </button>130 </div>131 );132};133134export const ModalBody = ({ children, className }: { children: React.ReactNode; className?: string }) => {135 return (136 <div className={cn("p-6 overflow-y-auto flex-1 bg-[#0A0A0A]", className)}>137 {children}138 </div>139 );140};141142export const ModalFooter = ({ children, className }: { children: React.ReactNode; className?: string }) => {143 return (144 <div className={cn("px-6 py-4 border-t border-white/5 bg-[#0F0F10] shrink-0 flex items-center justify-end gap-3", className)}>145 {children}146 </div>147 );148};