Success Modal
Original · freesuccess confirmation modal
byWeb InnoventixReact + Tailwind
modalsuccessmodals
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.jsonmodal-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 quoteSimilar components
Browse all →
Basic Modal
Originalbasic centred modal with overlay

Center Modal
Originalcentred modal with icon and actions

Form Modal
Originalmodal containing a form

Confirm Modal
Originalconfirm/destructive dialog

Alert Modal
Originalalert dialog with single action

Drawer Bottom Modal
Originalbottom sheet drawer

Side Drawer Modal
Originalright side drawer modal

Fullscreen Modal
Originalfullscreen modal

Image Lightbox Modal
Originalimage lightbox modal with nav

Video Modal
Originalvideo player modal

Scroll Modal
Originalmodal with long scrollable body

Steps Modal
Originalmulti-step modal flow

