Shared Element Transition
An element (like an image or card) seamlessly animates from its position in a list view into its final position in a detail view, bridging the context between two states.
Use
Image galleries opening to full screen
Avoid
Simple form steps
Safer Alternative
No safer default listed.
Risk Level
Low risk
Live Demo
Mountain Retreat
Escape to nature
Sunny Beach
Relax by the ocean
City Lights
Urban exploration
What It Is
An element (like an image or card) seamlessly animates from its position in a list view into its final position in a detail view, bridging the context between two states.
✓When to Use
- Image galleries opening to full screen
- App store cards expanding into detail pages
- Profile avatars moving to header banners
✕When NOT to Use
- Simple form steps
- Where the element drastically changes aspect ratio (can look distorted)
Configuration Tips
- 01Use Framer Motion layoutId for effortless shared element transitions
- 02Animate other surrounding content fading out quickly so they don't clash with the moving element
You've Seen It In
AI Implementation Prompts
Move from a fast scaffold to production details, then tune timing and edge states.
Create a shared element transition where clicking a card makes the image grow smoothly to cover the top of the detail page.
Reference Implementation
The same implementation approach used by the live preview, kept compact enough to inspect and adapt.
"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X } from 'lucide-react';
const items = [
{ id: '1', title: 'Mountain Retreat', subtitle: 'Escape to nature', color: 'bg-stone-200', text: 'Peaceful cabin surrounded by pine trees.' },
{ id: '2', title: 'Sunny Beach', subtitle: 'Relax by the ocean', color: 'bg-stone-300', text: 'White sand and crystal clear water.' },
{ id: '3', title: 'City Lights', subtitle: 'Urban exploration', color: 'bg-stone-400', text: 'Vibrant nightlife and modern architecture.' },
];
export function SharedElementTransitionDemo({ isPlaying = false }: { isPlaying?: boolean }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
useEffect(() => {
if (!isPlaying) {
setSelectedId(null);
return;
}
let index = 0;
const interval = setInterval(() => {
setSelectedId((currentSelectedId) => {
if (currentSelectedId) {
return null;
}
const nextId = items[index].id;
index = (index + 1) % items.length;
return nextId;
});
}, 1500);
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">
<div className="w-full max-w-sm grid gap-4 relative z-0">
{items.map(item => (
<motion.div
key={item.id}
layoutId={`card-${item.id}`}
onClick={() => setSelectedId(item.id)}
className={`p-4 rounded-xl cursor-pointer ${item.color} shadow-sm hover:shadow-md transition-shadow`}
>
<motion.h3 layoutId={`title-${item.id}`} className="font-medium text-stone-900">
{item.title}
</motion.h3>
<motion.p layoutId={`subtitle-${item.id}`} className="text-sm text-stone-600 mt-1">
{item.subtitle}
</motion.p>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 z-10 bg-white/40 backdrop-blur-sm flex items-center justify-center p-4"
onClick={() => setSelectedId(null)}
>
{items.filter(item => item.id === selectedId).map(item => (
<motion.div
key="modal"
layoutId={`card-${item.id}`}
className={`w-full max-w-sm rounded-2xl overflow-hidden ${item.color} shadow-xl relative cursor-default`}
onClick={e => e.stopPropagation()}
>
<div className="relative flex min-h-[200px] flex-col justify-end p-6">
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
onClick={() => setSelectedId(null)}
className="absolute top-4 right-4 p-2 bg-white/50 hover:bg-white/80 rounded-full transition-colors text-stone-700 backdrop-blur-md"
>
<X className="w-4 h-4" />
</motion.button>
<motion.h3 layoutId={`title-${item.id}`} className="text-2xl font-bold text-stone-900">
{item.title}
</motion.h3>
<motion.p layoutId={`subtitle-${item.id}`} className="text-stone-700 font-medium mt-1">
{item.subtitle}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 }}
className="mt-4 pt-4 border-t border-black/10 text-stone-800 leading-relaxed text-sm"
>
{item.text}
</motion.div>
</div>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
}