Retour au catalogue
Modal Form
Modale avec formulaire multi-champs, header, footer et boutons d'action.
modal-pagesmedium Both Responsive a11y
minimalcorporatesaasuniversalcentered
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";
interface FormField {
label: string;
type: "text" | "email" | "textarea" | "select";
placeholder: string;
options?: string[];
}
interface ModalFormProps {
title?: string;
description?: string;
fields?: FormField[];
submitLabel?: string;
}
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
export default function ModalForm({
title = "Formulaire",
description = "",
fields = [],
submitLabel = "Envoyer",
}: ModalFormProps) {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="min-h-[400px] flex items-center justify-center px-6" style={{ background: "var(--color-background)" }}>
<button onClick={() => setIsOpen(true)} className="px-4 py-2 text-sm font-medium rounded-lg" style={{ background: "var(--color-accent)", color: "var(--color-background)" }}>
Ouvrir le formulaire
</button>
<AnimatePresence>
{isOpen && (
<>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-40" style={{ background: "rgba(0,0,0,0.5)" }} onClick={() => setIsOpen(false)} />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.25, ease }}
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden"
style={{ background: "var(--color-background)", border: "1px solid var(--color-border)" }}
>
<div className="flex items-center justify-between px-6 py-4" style={{ borderBottom: "1px solid var(--color-border)" }}>
<div>
<h3 className="text-base font-semibold" style={{ color: "var(--color-foreground)" }}>{title}</h3>
{description && <p className="mt-0.5 text-sm" style={{ color: "var(--color-foreground-muted)" }}>{description}</p>}
</div>
<button onClick={() => setIsOpen(false)}><X size={18} style={{ color: "var(--color-foreground-light)" }} /></button>
</div>
<div className="px-6 py-5 flex flex-col gap-4">
{fields.map((field, i) => (
<div key={i}>
<label className="block text-sm font-medium mb-1.5" style={{ color: "var(--color-foreground)" }}>{field.label}</label>
{field.type === "textarea" ? (
<textarea rows={3} placeholder={field.placeholder} className="w-full px-3 py-2 text-sm rounded-lg bg-transparent outline-none resize-none" style={{ border: "1px solid var(--color-border)", color: "var(--color-foreground)" }} />
) : field.type === "select" ? (
<select className="w-full px-3 py-2 text-sm rounded-lg bg-transparent outline-none" style={{ border: "1px solid var(--color-border)", color: "var(--color-foreground)" }}>
<option>{field.placeholder}</option>
{field.options?.map((opt, oi) => <option key={oi}>{opt}</option>)}
</select>
) : (
<input type={field.type} placeholder={field.placeholder} className="w-full px-3 py-2 text-sm rounded-lg bg-transparent outline-none" style={{ border: "1px solid var(--color-border)", color: "var(--color-foreground)" }} />
)}
</div>
))}
</div>
<div className="flex items-center justify-end gap-3 px-6 py-4" style={{ borderTop: "1px solid var(--color-border)" }}>
<button onClick={() => setIsOpen(false)} className="px-4 py-2 text-sm font-medium rounded-lg" style={{ color: "var(--color-foreground-muted)" }}>Annuler</button>
<motion.button whileTap={{ scale: 0.97 }} className="px-4 py-2 text-sm font-medium rounded-lg" style={{ background: "var(--color-accent)", color: "var(--color-background)" }}>{submitLabel}</motion.button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}