Web InnoventixFreeCode

Success Modal

Original · free

success confirmation modal

byWeb InnoventixReact + Tailwind
modalsuccessmodals
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/modal-success.json
modal-success.tsx
"use client";

import {
  useCallback,
  useEffect,
  useId,
  useRef,
  useState,
  type CSSProperties,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type CSSVars = CSSProperties & Record<`--${string}`, string>;

type Spark = {
  id: number;
  x: number;
  y: number;
  color: string;
  delay: number;
};

const SPARKS: Spark[] = [
  { id: 0, x: 0, y: -46, color: "bg-emerald-400", delay: 40 },
  { id: 1, x: 38, y: -30, color: "bg-sky-400", delay: 90 },
  { id: 2, x: 48, y: 6, color: "bg-amber-400", delay: 20 },
  { id: 3, x: 34, y: 40, color: "bg-violet-400", delay: 120 },
  { id: 4, x: -4, y: 50, color: "bg-emerald-300", delay: 70 },
  { id: 5, x: -40, y: 36, color: "bg-sky-300", delay: 30 },
  { id: 6, x: -50, y: 0, color: "bg-amber-300", delay: 110 },
  { id: 7, x: -34, y: -34, color: "bg-violet-300", delay: 60 },
];

const FOCUSABLE =
  'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';

export default function ModalSuccess() {
  const [open, setOpen] = useState(false);
  const [copied, setCopied] = useState(false);

  const reduceMotion = useReducedMotion();

  const triggerRef = useRef<HTMLButtonElement | null>(null);
  const panelRef = useRef<HTMLDivElement | null>(null);
  const closeRef = useRef<HTMLButtonElement | null>(null);
  const copyTimer = useRef<number | null>(null);

  const titleId = useId();
  const descId = useId();

  const reference = "AUR-4827-QL93";

  const close = useCallback(() => setOpen(false), []);

  const handleCopy = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(reference);
      setCopied(true);
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
      copyTimer.current = window.setTimeout(() => setCopied(false), 2200);
    } catch {
      setCopied(false);
    }
  }, [reference]);

  // Lock body scroll while the dialog is open.
  useEffect(() => {
    if (!open) return;
    const previous = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = previous;
    };
  }, [open]);

  // Move focus into the dialog on open, restore it to the trigger on close.
  useEffect(() => {
    if (!open) return;
    const previouslyFocused = document.activeElement as HTMLElement | null;
    const raf = window.requestAnimationFrame(() => closeRef.current?.focus());
    return () => {
      window.cancelAnimationFrame(raf);
      const target = previouslyFocused ?? triggerRef.current;
      target?.focus?.();
    };
  }, [open]);

  // Escape to close + Tab focus trap.
  useEffect(() => {
    if (!open) return;
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        event.preventDefault();
        close();
        return;
      }
      if (event.key !== "Tab") return;
      const panel = panelRef.current;
      if (!panel) return;
      const nodes = Array.from(
        panel.querySelectorAll<HTMLElement>(FOCUSABLE),
      ).filter((el) => el.offsetParent !== null || el === document.activeElement);
      if (nodes.length === 0) return;
      const first = nodes[0];
      const last = nodes[nodes.length - 1];
      const active = document.activeElement;
      if (event.shiftKey && active === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && active === last) {
        event.preventDefault();
        first.focus();
      }
    };
    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, [open, close]);

  useEffect(() => {
    return () => {
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
    };
  }, []);

  const overlayTransition = reduceMotion
    ? { duration: 0.12 }
    : { duration: 0.24, ease: [0.22, 1, 0.36, 1] as const };

  const panelTransition = reduceMotion
    ? { duration: 0.12 }
    : { duration: 0.42, ease: [0.16, 1, 0.3, 1] as const };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-6 py-24 text-slate-900 sm:px-10 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes msx-badge-in {
          0% { transform: scale(0.4); opacity: 0; }
          55% { transform: scale(1.12); opacity: 1; }
          100% { transform: scale(1); opacity: 1; }
        }
        @keyframes msx-check-draw {
          from { stroke-dashoffset: 52; }
          to { stroke-dashoffset: 0; }
        }
        @keyframes msx-ring-out {
          0% { transform: scale(0.55); opacity: 0.55; }
          100% { transform: scale(2); opacity: 0; }
        }
        @keyframes msx-spark-fly {
          0% { transform: translate3d(0,0,0) scale(1); opacity: 0; }
          20% { opacity: 1; }
          100% { transform: translate3d(var(--msx-x), var(--msx-y), 0) scale(0.2); opacity: 0; }
        }
        .msx-badge { animation: msx-badge-in 640ms cubic-bezier(0.16,1,0.3,1) both; }
        .msx-check { stroke-dasharray: 52; stroke-dashoffset: 52; animation: msx-check-draw 520ms cubic-bezier(0.65,0,0.35,1) 260ms forwards; }
        .msx-ring { animation: msx-ring-out 1400ms ease-out 220ms forwards; }
        .msx-spark { animation: msx-spark-fly 900ms cubic-bezier(0.22,1,0.36,1) forwards; }
        @media (prefers-reduced-motion: reduce) {
          .msx-badge { animation: none; }
          .msx-check { stroke-dashoffset: 0; animation: none; }
          .msx-ring { animation: none; opacity: 0; }
          .msx-spark { animation: none; opacity: 0; }
        }
      `}</style>

      {/* ambient backdrop texture */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_0%,#a7f3d0,transparent_55%)] opacity-70 dark:opacity-20"
      />

      <div className="relative mx-auto flex max-w-2xl flex-col items-center text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-medium tracking-wide text-emerald-700 uppercase dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300">
          <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
          Aurora · Billing
        </span>

        <h1 className="mt-6 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-slate-50">
          Confirm the checkout flow
        </h1>
        <p className="mt-4 max-w-md text-balance text-slate-600 dark:text-slate-400">
          Press the button to complete a purchase and preview the success
          confirmation your customers see after paying.
        </p>

        <button
          ref={triggerRef}
          type="button"
          onClick={() => setOpen(true)}
          className="mt-8 inline-flex items-center gap-2 rounded-xl bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/20 transition hover:bg-slate-800 focus-visible:ring-2 focus-visible:ring-slate-900 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 focus-visible:outline-none dark:bg-white dark:text-slate-900 dark:shadow-black/40 dark:hover:bg-slate-100 dark:focus-visible:ring-white dark:focus-visible:ring-offset-slate-950"
        >
          Complete purchase
          <svg
            width="16"
            height="16"
            viewBox="0 0 24 24"
            fill="none"
            aria-hidden="true"
          >
            <path
              d="M5 12h14M13 6l6 6-6 6"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
          </svg>
        </button>
      </div>

      <AnimatePresence>
        {open ? (
          <motion.div
            key="msx-overlay"
            className="fixed inset-0 z-50 flex items-end justify-center bg-slate-900/55 p-4 backdrop-blur-sm sm:items-center"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={overlayTransition}
            onMouseDown={(event) => {
              if (event.target === event.currentTarget) close();
            }}
          >
            <motion.div
              ref={panelRef}
              role="dialog"
              aria-modal="true"
              aria-labelledby={titleId}
              aria-describedby={descId}
              className="relative w-full max-w-md overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl shadow-slate-900/25 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/60"
              initial={{
                opacity: 0,
                y: reduceMotion ? 0 : 24,
                scale: reduceMotion ? 1 : 0.96,
              }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{
                opacity: 0,
                y: reduceMotion ? 0 : 12,
                scale: reduceMotion ? 1 : 0.98,
              }}
              transition={panelTransition}
            >
              <button
                ref={closeRef}
                type="button"
                onClick={close}
                aria-label="Close confirmation"
                className="absolute top-4 right-4 z-10 inline-flex h-9 w-9 items-center justify-center rounded-lg text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:outline-none dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-200"
              >
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                  <path
                    d="M6 6l12 12M18 6L6 18"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                  />
                </svg>
              </button>

              {/* header / seal */}
              <div className="flex flex-col items-center px-8 pt-10 text-center">
                <div className="relative flex h-24 w-24 items-center justify-center">
                  {!reduceMotion ? (
                    <>
                      <span
                        aria-hidden="true"
                        className="msx-ring absolute inset-0 rounded-full border border-emerald-400/70"
                      />
                      <span
                        aria-hidden="true"
                        className="msx-ring absolute inset-0 rounded-full border border-emerald-300/50"
                        style={{ animationDelay: "460ms" }}
                      />
                      {SPARKS.map((spark) => (
                        <span
                          key={spark.id}
                          aria-hidden="true"
                          className={`msx-spark absolute top-1/2 left-1/2 h-1.5 w-1.5 rounded-full ${spark.color}`}
                          style={
                            {
                              "--msx-x": `${spark.x}px`,
                              "--msx-y": `${spark.y}px`,
                              animationDelay: `${380 + spark.delay}ms`,
                            } as CSSVars
                          }
                        />
                      ))}
                    </>
                  ) : null}

                  <div className="msx-badge relative flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-emerald-600 shadow-lg shadow-emerald-500/30 ring-8 ring-emerald-50 dark:ring-emerald-950/50">
                    <svg
                      width="40"
                      height="40"
                      viewBox="0 0 24 24"
                      fill="none"
                      aria-hidden="true"
                    >
                      <path
                        className="msx-check"
                        d="M5 12.5l4.2 4.2L19 7"
                        stroke="white"
                        strokeWidth="2.4"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      />
                    </svg>
                  </div>
                </div>

                <h2
                  id={titleId}
                  className="mt-6 text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-50"
                >
                  Payment confirmed
                </h2>
                <p
                  id={descId}
                  className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400"
                >
                  Your Aurora Pro plan is active. A receipt is on its way to{" "}
                  <span className="font-medium text-slate-800 dark:text-slate-200">
                    a•••@studio.co
                  </span>
                  .
                </p>
              </div>

              {/* receipt detail card */}
              <div className="mx-8 mt-6 rounded-xl border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950/50">
                <dl className="divide-y divide-slate-200 text-sm dark:divide-slate-800">
                  <div className="flex items-center justify-between px-4 py-3">
                    <dt className="text-slate-500 dark:text-slate-400">Plan</dt>
                    <dd className="font-medium text-slate-900 dark:text-slate-100">
                      Aurora Pro · Annual
                    </dd>
                  </div>
                  <div className="flex items-center justify-between px-4 py-3">
                    <dt className="text-slate-500 dark:text-slate-400">
                      Amount paid
                    </dt>
                    <dd className="font-medium text-slate-900 dark:text-slate-100">
                      $189.00 USD
                    </dd>
                  </div>
                  <div className="flex items-center justify-between px-4 py-3">
                    <dt className="text-slate-500 dark:text-slate-400">
                      Reference
                    </dt>
                    <dd>
                      <button
                        type="button"
                        onClick={handleCopy}
                        className="group inline-flex items-center gap-2 rounded-md px-1.5 py-0.5 font-mono text-xs text-slate-900 transition hover:bg-slate-200/70 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:outline-none dark:text-slate-100 dark:hover:bg-slate-800"
                      >
                        {reference}
                        {copied ? (
                          <svg
                            width="14"
                            height="14"
                            viewBox="0 0 24 24"
                            fill="none"
                            aria-hidden="true"
                            className="text-emerald-600 dark:text-emerald-400"
                          >
                            <path
                              d="M5 12.5l4.2 4.2L19 7"
                              stroke="currentColor"
                              strokeWidth="2.4"
                              strokeLinecap="round"
                              strokeLinejoin="round"
                            />
                          </svg>
                        ) : (
                          <svg
                            width="14"
                            height="14"
                            viewBox="0 0 24 24"
                            fill="none"
                            aria-hidden="true"
                            className="text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300"
                          >
                            <rect
                              x="9"
                              y="9"
                              width="11"
                              height="11"
                              rx="2"
                              stroke="currentColor"
                              strokeWidth="2"
                            />
                            <path
                              d="M5 15V5a2 2 0 012-2h10"
                              stroke="currentColor"
                              strokeWidth="2"
                              strokeLinecap="round"
                            />
                          </svg>
                        )}
                        <span className="sr-only" aria-live="polite">
                          {copied ? "Reference copied to clipboard" : ""}
                        </span>
                      </button>
                    </dd>
                  </div>
                </dl>
              </div>

              {/* actions */}
              <div className="flex flex-col gap-3 px-8 pt-6 pb-8 sm:flex-row-reverse">
                <button
                  type="button"
                  onClick={close}
                  className="inline-flex flex-1 items-center justify-center gap-2 rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-emerald-700 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:focus-visible:ring-offset-slate-900"
                >
                  Go to dashboard
                </button>
                <button
                  type="button"
                  onClick={close}
                  className="inline-flex flex-1 items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-slate-500 dark:focus-visible:ring-offset-slate-900"
                >
                  View receipt
                </button>
              </div>
            </motion.div>
          </motion.div>
        ) : null}
      </AnimatePresence>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →