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>
  );
}

Avis

Modal Form — React Modal-pages Section — Incubator