Modal Enter / Exit
Choose modal motion when the interaction should pause the page without feeling like a new page.
Use
Focused decisions, compact editing flows, and confirmations that should preserve page context.
Avoid
Avoid for long workflows, full-page forms, or navigation that deserves its own route.
Safer Alternative
Fade TransitionRisk Level
High attention
Timing
Backdrop fade around 150-220ms; modal scale/fade around 180-260ms.
Easing
Use ease-out or a restrained spring on entry; avoid bouncy exits for serious tasks.
Risk
Live Demo
What It Is
A combination of a fading background overlay and a modal window that scales and fades into the center of the viewport.
Decision Guidance
Choose modal motion when the interaction should pause the page without feeling like a new page.
Best For
Focused decisions, compact editing flows, and confirmations that should preserve page context.
Avoid When
Avoid for long workflows, full-page forms, or navigation that deserves its own route.
Timing
Backdrop fade around 150-220ms; modal scale/fade around 180-260ms.
Easing
Use ease-out or a restrained spring on entry; avoid bouncy exits for serious tasks.
Risk Tags
✓When to Use
- Confirmations and alerts
- Editing details in a dashboard
- Sign-in prompts
✕When NOT to Use
- Full-page takeovers (use slide instead)
Configuration Tips
- 01Modal should initial from scale 0.95 and opacity 0
- 02Backsplash should fade in (opacity 0 to 0.5) over 200ms
You've Seen It In
AI Implementation Prompts
Move from a fast scaffold to production details, then tune timing and edge states.
Animate this popup modal. The background should fade to dark, and the modal card should scale up slightly and fade in exactly in the middle.
Reference Implementation
The same implementation approach used by the live preview, kept compact enough to inspect and adapt.
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';
export function ModalEnterExitDemo({ isPlaying = false }: { isPlaying?: boolean }) {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (!isPlaying) {
setIsOpen(false);
return;
}
const interval = setInterval(() => {
setIsOpen((prev) => !prev);
}, 2000);
return () => clearInterval(interval);
}, [isPlaying]);
return (
<div className="relative flex h-64 w-full items-center justify-center overflow-hidden rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
{"text-stone-400 italic">/* Underlay Dashboard Mock */}
<div className="grid grid-cols-2 gap-4 opacity-30">
<div className="h-24 w-24 rounded-xl bg-stone-200" />
<div className="h-24 w-24 rounded-xl bg-stone-200" />
</div>
<AnimatePresence>
{isOpen && (
<div className="absolute inset-0 z-10 flex items-center justify-center">
{"text-stone-400 italic">/* Backdrop Fade */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-stone-900/20 backdrop-blur-sm"
/>
{"text-stone-400 italic">/* Modal Scale In */}
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 10 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 10 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
className="relative z-10 flex w-48 flex-col items-center gap-3 rounded-2xl bg-white p-6 shadow-xl ring-1 ring-stone-200"
>
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-red-50 mb-2">
<div className="h-5 w-5 rounded-full bg-red-400" />
</div>
<div className="h-4 w-24 rounded bg-stone-800" />
<div className="h-3 w-32 rounded bg-stone-300" />
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}