Morphing Dialog
A card that smoothly expands into a full-screen dialog/modal.
Preview
Live interactive preview.
Installation
Add this component to your project using the CLI:
npx -y vui-registry-cli-v1@latest add morphing-dialogSource Code
'use client'
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { X, Maximize2, ArrowRight } from 'lucide-react'
// Professional easing for a "smooth" feel
const transition = {
type: 'spring',
stiffness: 150,
damping: 24,
mass: 0.8,
} as const
export default function MorphingDialog() {
const [isOpen, setIsOpen] = useState(false)
// Close on Escape key
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setIsOpen(false)
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [])
return (
<div className="w-full min-h-[600px] flex items-center justify-center relative overflow-hidden p-8 font-sans">
<div className="absolute inset-0 bg-neutral-50/50 dark:bg-neutral-950/50" />
<div className="absolute inset-0 bg-grid-black/[0.02] dark:bg-grid-white/[0.02] bg-[size:32px_32px]" />
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop with a smoother blur fade */}
<motion.div
initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
animate={{ opacity: 1, backdropFilter: 'blur(12px)' }}
exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
onClick={() => setIsOpen(false)}
className="absolute inset-0 bg-white/40 dark:bg-black/60"
/>
<motion.div
layoutId="card-container"
transition={transition}
className="w-full max-w-3xl flex flex-col md:flex-row bg-white dark:bg-neutral-900 overflow-hidden shadow-[0_32px_64px_-16px_rgba(0,0,0,0.2)] dark:shadow-[0_32px_64px_-16px_rgba(0,0,0,0.6)] relative z-10 border border-neutral-200 dark:border-white/10"
style={{ borderRadius: 24 }}
>
<motion.div layoutId="image-container" className="relative w-full md:w-1/2 aspect-[3/4] overflow-hidden">
<motion.img
layout
src="/43/2.jpg"
alt="Porsche 911"
className="w-full h-full object-cover"
/>
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
onClick={() => setIsOpen(false)}
className="absolute top-5 right-5 p-2.5 bg-white/20 hover:bg-white/30 text-white rounded-full backdrop-blur-xl transition-colors border border-white/20 z-20"
>
<X size={20} />
</motion.button>
<div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/90 via-black/40 to-transparent">
<motion.h2 layoutId="title" className="text-4xl font-bold text-white tracking-tight uppercase">Porsche 911</motion.h2>
<motion.p layoutId="subtitle" className="text-neutral-200 font-medium tracking-wide text-sm mt-2 uppercase">GT3 MSRP: $161,100</motion.p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ delay: 0.1 }}
className="w-full md:w-1/2 p-8 flex flex-col justify-center space-y-6"
>
<div className="flex gap-3">
{['502 HP', '0-60 3.2s', '198 MPH'].map((spec) => (
<span key={spec} className="px-3 py-1 rounded-full bg-neutral-100 dark:bg-neutral-800 text-[10px] font-medium tracking-widest uppercase text-neutral-500 dark:text-neutral-400 border border-neutral-200 dark:border-white/5">
{spec}
</span>
))}
</div>
<p className="text-neutral-500 dark:text-neutral-400 text-sm leading-relaxed font-light">
The 911 GT3 is the closest you can get to a race car for the street. With its naturally aspirated 4.0-liter flat-six engine producing 502 horsepower, it delivers an uncompromising, visceral driving experience built for the track.
</p>
<div className="pt-2 flex flex-col sm:flex-row gap-3">
<button className="flex-1 px-6 py-3 bg-neutral-950 dark:bg-white text-white dark:text-black rounded-xl font-medium tracking-wide text-sm hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2">
Configure <ArrowRight size={16} />
</button>
<button className="flex-1 px-6 py-3 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white rounded-xl font-medium tracking-wide text-sm hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors">
Specs
</button>
</div>
</motion.div>
</motion.div>
</div>
)}
</AnimatePresence>
{!isOpen && (
<motion.div
layoutId="card-container"
transition={transition}
onClick={() => setIsOpen(true)}
whileHover={{ y: -4 }}
whileTap={{ scale: 0.98 }}
className="w-full max-w-[480px] flex flex-row bg-white dark:bg-neutral-900 cursor-pointer group shadow-xl hover:shadow-2xl border border-neutral-200 dark:border-white/10 overflow-hidden"
style={{ borderRadius: 24 }}
>
<motion.div layoutId="image-container" className="relative w-2/5 aspect-[3/4] overflow-hidden">
<motion.img
layout
src="/43/2.jpg"
alt="Porsche 911"
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute top-3 right-3 p-1.5 bg-black/40 text-white rounded-full backdrop-blur-md opacity-0 group-hover:opacity-100 transition-opacity">
<Maximize2 size={14} />
</div>
</motion.div>
<div className="w-3/5 p-6 flex flex-col justify-center">
<motion.h2 layoutId="title" className="text-2xl font-bold tracking-tight uppercase text-neutral-900 dark:text-white">Porsche 911</motion.h2>
<motion.p layoutId="subtitle" className="text-neutral-500 dark:text-neutral-400 font-medium tracking-wide text-sm mt-2 uppercase">GT3 MSRP: $161,100</motion.p>
</div>
</motion.div>
)}
</div>
)
}Dependencies
framer-motion: latestlucide-react: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| trigger | ReactNode | null | The element that opens the dialog. |
| content | ReactNode | null | The content displayed inside the dialog. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.