Morphing Dialog
A card that smoothly expands into a full-screen dialog/modal.
Installation
Add this component to your project using the CLI:
terminal
npx vui-registry-cli-v1 add morphing-dialogSource Code
morphing-dialog.tsx
'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,
}
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-2xl 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: 32 }}
>
<motion.div layoutId="image-container" className="relative h-72 sm:h-80 overflow-hidden">
<motion.img
layout
src="https://images.unsplash.com/photo-1617788138017-80ad40651399?q=80&w=2070&auto=format&fit=crop"
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-8 bg-gradient-to-t from-black/80 via-black/20 to-transparent">
<motion.h2 layoutId="title" className="text-3xl font-bold text-white">Porsche 911 GT3</motion.h2>
<motion.p layoutId="subtitle" className="text-neutral-200/90 text-sm mt-1">Precision. Power. Performance.</motion.p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 }}
className="p-8 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.5 rounded-full bg-neutral-100 dark:bg-neutral-800 text-[11px] font-bold tracking-wider uppercase text-neutral-500 dark:text-neutral-400 border border-neutral-200 dark:border-white/5">
{spec}
</span>
))}
</div>
<p className="text-neutral-600 dark:text-neutral-400 leading-relaxed font-light">
The 911 GT3 with Touring Package. It hides its performance potential. But not its ambition. A high-performance athlete that does not show off its talent, but loves to demonstrate it.
</p>
<div className="pt-2 flex gap-3">
<button className="flex-[2] px-6 py-4 bg-neutral-950 dark:bg-white text-white dark:text-black rounded-2xl font-bold hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2">
Configure Now <ArrowRight size={18} />
</button>
<button className="flex-1 px-6 py-4 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white rounded-2xl font-bold 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-[380px] 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: 32 }}
>
<motion.div layoutId="image-container" className="relative h-64 overflow-hidden">
<motion.img
layout
src="https://images.unsplash.com/photo-1617788138017-80ad40651399?q=80&w=2070&auto=format&fit=crop"
alt="Porsche 911"
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute top-4 right-4 p-2 bg-black/40 text-white rounded-full backdrop-blur-md opacity-0 group-hover:opacity-100 transition-opacity">
<Maximize2 size={16} />
</div>
</motion.div>
<div className="p-6">
<motion.h2 layoutId="title" className="text-xl font-bold text-neutral-900 dark:text-white">Porsche 911 GT3</motion.h2>
<motion.p layoutId="subtitle" className="text-neutral-500 dark:text-neutral-400 text-sm">Precision. Power. Performance.</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. |
Context Worth Keeping In Orbit
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.

