How to use LayoutId and AnimatePresence for smooth layout changes
In this blog, I have given an example on how to use framer-motion especially layoutId and AnimatePresence to create an animated product card perfect for use in an e-commerce website.
If you are wondering how to make smooth morphing or transitions between components using motion then this blog is for you. In this blog we will learn proper use of layoutId and AnimatePresence by building an animated product card. See how it works.
The Foundation: Setting Up
First things first, make your component as client component and import motion for use it. Remember to import from 'motion/react' not 'framer-motion'
'use client'
import React, { useState } from 'react'
import {AnimatePresence, motion} from 'motion/react'
First, let’s define what makes a product card. In TypeScript, we’re fancy like that:
interface ProductCardProps {
title: string
description: string
features: string[]
image: string
price: string
}
const ProductCardData : ProductCardProps[] = [
{
title: 'Premium Wireless Headphones',
description: 'High-fidelity audio with active noise cancellation and premium comfort.',
features: [
'40-hour Battery Life',
'Active Noise Cancellation',
'Premium Sound Quality',
'Comfortable Fit',
],
image: 'https://images.pexels.com/photos/3394650/pexels-photo-3394650.jpeg',
price: '$299.99',
},
{
title: 'Smart Fitness Watch',
description: 'Track your health and stay connected with this advanced smartwatch.', features: [
'Heart Rate Monitor',
'Sleep Tracking',
'GPS Navigation',
'Water Resistant',
],
image: 'https://images.pexels.com/photos/437037/pexels-photo-437037.jpeg',
price: '$199.99',
},
]
Now the important part begins:
function ProductCard() {
const [isClicked, setIsClicked] = useState <ProductCardProps | null>(null)
return (
// Our main code goes here
)
}
We’re tracking which product is selected using useState. Initially, nobody’s selected anything so we are starting with null.
<div className='min-h-screen min-w-screen flex items-center justify-center bg-neutral-50 relative'>
<div className='flex items-center justify-center gap-8 cursor-pointer relative'>
{ProductCardData.map((data,idx) => {
return (
<motion.div key={idx} className='w-96 h-[420px] bg-white/10 backdrop-blur-md rounded-xl border border-neutral-50'
layoutId={`card-${data.title}`}
onClick={() => setIsClicked(data)}
>
<div className='relative h-full bg-white mask-b-from-80%'>
<img src={data.image} alt={data.title} className='w-full h-full object-cover mb-4 p-1 rounded-xl'/>
<motion.div className='px-4 absolute bottom-0 z-10' layoutId={`card-${data.price}`}>
<h2 className='text-4xl font-bold mb-2 text-black h-18'>{data.title}</h2>
</motion.div>
</div>
</motion.div>
)
})
}
</div>
</div>
- Outer <div> </div> is for setting my screen. You can either ignore this or set up for your usecase.
- Inner <div> </div> is for showing the product cards in row. We also have set this div as relative because we want to place expanded content by giving an absolute positioning.
- We have used .map() function for showing the cards. Don't forget to give key props. It's important.
- The important part here is we have given each <div> </div> and important HTML tags an unique layoutId . Behind the scene framer motion will use these layoutIds for smooth morphing from normal state to expanded state.
Now inside the inner div we will write the code that will be used for showing the expanded state of each card.
<AnimatePresence>
{isClicked && (
<motion.div
layoutId={`card-${isClicked.title}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{duration:0.3, type:'spring', stiffness:100, damping:20}}
className='fixed inset-0 m-auto w-[500px] h-[600px] bg-white/90 backdrop-blur-md rounded-xl border border-neutral-50 z-10'
onClick={(e) => {
e.stopPropagation()
setIsClicked(null)
}}
>
<div className='w-full h-full mx-auto'>
<img src={isClicked.image} alt={isClicked.title} className='w-full h-64 object-cover mb-4 p-1 rounded-xl'/>
<motion.div className='px-6' layoutId={`card-${isClicked.price}`}>
<h2 className='text-3xl font-bold mb-2 text-neutral-900'>{isClicked.title}</h2>
<div className='text-xl font-light text-neutral-900 mb-4'>{isClicked.price}</div>
<p className='text-neutral-900 mb-4'>{isClicked.description}</p>
<ul className='list-disc list-inside mb-6 text-neutral-900'>
{isClicked.features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
- We wrap the whole section inside AnimatePresence. This is a powerful feature from 'motion/react' (earlier this was 'framer-motion'). With this you get the access of exit prop. Whenever a component mounts or remounts, key is changed AnimatePresence gives you a control on how you want when the component exists from the React tree.
- Inside this we have designed the the card design with more data for the clicked card that smoothly expands on your screen.
- Look carefully here also we have given the same layoutId prop as the from the unexpanded state cards. This is must. You have to give the same layoutId so that framer-motion exactly knows which components morphs to which state.
So that's all. Here is the full version.
'use client'
import React, { useState } from 'react'
import {AnimatePresence, motion} from 'motion/react'
interface ProductCardProps {
title: string
description: string
features: string[]
image: string
price: string
}
const ProductCardData : ProductCardProps[] = [
{
title: 'Premium Wireless Headphones',
description: 'High-fidelity audio with active noise cancellation and premium comfort.',
features: [
'40-hour Battery Life',
'Active Noise Cancellation',
'Premium Sound Quality',
'Comfortable Fit',
],
image: 'https://images.pexels.com/photos/3394650/pexels-photo-3394650.jpeg',
price: '$299.99',
},
{
title: 'Smart Fitness Watch',
description: 'Track your health and stay connected with this advanced smartwatch.',
features: [
'Heart Rate Monitor',
'Sleep Tracking',
'GPS Navigation',
'Water Resistant',
],
image: 'https://images.pexels.com/photos/437037/pexels-photo-437037.jpeg',
price: '$199.99',
},
]
function ProductCard() {
const [isClicked, setIsClicked] = useState <ProductCardProps | null>(null)
return (
<div className='min-h-screen min-w-screen flex items-center justify-center bg-neutral-50 relative'>
<div className='flex items-center justify-center gap-8 cursor-pointer relative'>
<AnimatePresence>
{isClicked && (
<motion.div
layoutId={`card-${isClicked.title}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{duration:0.3, type:'spring', stiffness:100, damping:20}}
className='fixed inset-0 m-auto w-[500px] h-[600px] bg-white/90 backdrop-blur-md rounded-xl border border-neutral-50 z-10'
onClick={(e) => {
e.stopPropagation()
setIsClicked(null)
}}
>
<div className='w-full h-full mx-auto'>
<img src={isClicked.image} alt={isClicked.title} className='w-full h-64 object-cover mb-4 p-1 rounded-xl'/>
<motion.div className='px-6' layoutId={`card-${isClicked.price}`}>
<h2 className='text-3xl font-bold mb-2 text-neutral-900'>{isClicked.title}</h2>
<div className='text-xl font-light text-neutral-900 mb-4'>{isClicked.price}</div>
<p className='text-neutral-900 mb-4'>{isClicked.description}</p>
<ul className='list-disc list-inside mb-6 text-neutral-900'>
{isClicked.features.map((feature, idx) => (
<li key={idx}>{feature}</li>
))}
</ul>
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
{ProductCardData.map((data,idx) => {
return (
<motion.div key={idx} className='w-96 h-[420px] bg-white/10 backdrop-blur-md rounded-xl border border-neutral-50'
layoutId={`card-${data.title}`}
onClick={() => setIsClicked(data)}
>
<div className='relative h-full bg-white mask-b-from-80%'>
<img src={data.image} alt={data.title} className='w-full h-full object-cover mb-4 p-1 rounded-xl'/>
<motion.div className='px-4 absolute bottom-0 z-10' layoutId={`card-${data.price}`}>
<h2 className='text-4xl font-bold mb-2 text-black h-18'>{data.title}</h2>
</motion.div>
</div>
</motion.div>
)
})
}
</div>
</div>
)
}
export default ProductCard
To cut the long story short,
- Wrap AnimatePresence for exit animation.
- Give unique layoutId to those HTML tags which you want to animate.
- Make sure the layoutId remains same for the tags which you want to animate.
Hope, you learnt something new. Share with your friends. Tag me on twitter.
I will be back with new concepts. See you next time..
