Morphing Dialog
An expandable card trigger that fluidly morphs into a centered dialog without jarring popup animations.
Live preview from the library's source
Install
From EasyUI
npx shadcn@latest add Surajmaurya1/easyui/morphing-dialogFrom this collection
npx shadcn@latest add https://allcomps.vercel.app/r/easyui--morphing-dialog.jsonCode
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X } from 'lucide-react';
import { cn } from '../../lib/utils';
import { motionTransitions } from '../../lib/motion-tokens';
export interface MorphingDialogProps {
id?: string;
trigger: (open: () => void) => React.ReactNode;
title: string;
subtitle?: string;
children: React.ReactNode;
className?: string;
}
export const MorphingDialog: React.FC<MorphingDialogProps> = ({
id = 'morph-dialog',
trigger,
title,
subtitle,
children,
className,
}) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setIsOpen(false);
};
if (isOpen) {
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen]);
return (
<>
<div className="inline-block">
{trigger(() => setIsOpen(true))}
</div>
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={motionTransitions.easeSoft}
onClick={() => setIsOpen(false)}
className="fixed inset-0 bg-black/70 backdrop-blur-md"
/>
{/* Expanded Morphing Surface */}
<motion.div
layoutId={id}
transition={motionTransitions.springMorph}
className={cn(
'relative w-full max-w-lg rounded-2xl border border-[#1F1F1F] bg-[#0E0E0E] p-6 sm:p-8 shadow-[0_20px_50px_rgba(0,0,0,0.6)] z-10 overflow-hidden',
className
)}
>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div>
<motion.h3
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
className="text-lg sm:text-xl font-semibold text-[#FAFAFA] tracking-tight"
>
{title}
</motion.h3>
{subtitle && (
<motion.p
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-xs sm:text-sm text-[#A1A1A1] mt-1"
>
{subtitle}
</motion.p>
)}
</div>
<button
onClick={() => setIsOpen(false)}
className="p-1.5 rounded-lg text-[#525252] hover:text-[#FAFAFA] hover:bg-[#141414] transition-colors focus-ring cursor-pointer"
aria-label="Close dialog"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Body */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
className="mt-4"
>
{children}
</motion.div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
);
};
Other dialogs & modals
The same kind of component from the other libraries in the collection.