Grid Card animation with framer motion
A simple layout animation with framer motion
Requirements
A simple layout animation with framer motion and react. Animating the active card in a grid of cards can help the user to focus on the content of the card. This is a simple example of how to do it.
This post assumes that you have a basic understanding of React. If you are new to React, you can follow the React getting started guide. We will also use tailwindcss for styling and framer-motion a popular animation library for React.
Setup
Dependencies Installation
We will install the required dependencies for the project.
yarn add framer-motion tailwindcss postcss autoprefixer clsx
Styling the Grid Card
We will use tailwindcss to style the tabs. We will create a new file tailwind.config.js
in the root of the project and add the following code.
tailwind.config.js
module.exports = {
experimental: {
optimizeUniversalDefaults: true
},
content: ['./src/**/*.{js,jsx,ts,tsx,vue,mdx,md}'],
darkMode: 'class',
theme: {
extend: {}
},
plugins: []
}
We will also create a new file postcss.config.js
in the root of the project and add the following code.
postcss.config.js
module.exports = {
plugins: {
'tailwindcss/nesting': {}, // enable css nesting
tailwindcss: {},
autoprefixer: {}
}
}
We will also update the tailwind.css
file in the styles
folder with the following code.
styles/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Creating the Component
Data for the component
We will create a simple array that will hold the data for the component.
data.ts
export interface ProductI {
id: string
name: string
price: number
image: {
url: string
}
type: string
weight: number
description: string
createdAt: string
updatedAt: string
}
export const data: ProductI[] = [
{
id: 'ecdc8405-87a9-4950-872e-885e27311481',
name: 'Battlefield',
price: 88.99,
type: 'book',
weight: 0.07,
description: 'Game, Thriller, Action',
image: {
url: '--image-url--'
},
createdAt: '2023-04-30T18:54:35.564Z',
updatedAt: '2023-04-30T18:54:35.564Z'
},
{
id: 'e8fb7251-6d54-46f0-995f-28845fc6cf30',
name: "Assasin's Creed",
price: 87.99,
type: 'book',
weight: 0.08,
description: 'Thriller, Game, Action',
image: {
url: '--image-url--'
},
createdAt: '2023-04-30T18:55:30.687Z',
updatedAt: '2023-04-30T18:55:30.687Z'
}
]
Grid Card Component
We will create a new file GridCard.tsx
in the components
folder and add the following code.
components/GridCard.tsx
import { useState } from 'react'
import Image from 'next/image' // nextjs image component
import { data, ProductI } from '---location-of-data---'
import { motion } from 'framer-motion'
export default function AnimatedGridComponent() {
const [selectedProduct, setSelectedProduct] = useState<ProductI | null>(null)
return (
<main className="relative min-h-[60vh] border border-neutral-800">
{/* body */}
<section className="mx-auto max-w-5xl px-2 py-7 max-lg:mx-5 md:px-12 xl:max-w-7xl">
<div className="">
<div className="xsm:grid-cols-[repeat(auto-fill,minmax(320px,1fr))] grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-8 text-xs">
{products.map(product => (
<motion.div
key={product.id}
layoutId={product.id}
className="space-y-7 border border-neutral-800 p-4 text-xs"
>
{/* body */}
<div className="space-y-2">
<div className="line-clamp-1">{product.id}</div>
<div className="font-bold uppercase">{product.name}</div>
<div className="">$ {product.price}</div>
<div className="text-xs">Weight : {product.weight} KG</div>
{/* actions */}
<div className="">
<button
type="button"
className="bg-neutral-300 px-2 py-1 text-xs font-bold uppercase dark:bg-neutral-700"
onClick={() => setSelectedProduct(product)}
>
View Details
</button>
</div>
</div>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedProduct && (
<motion.div className="absolute inset-0 flex items-center justify-center">
<motion.button
className="absolute inset-0 block h-full w-full bg-neutral-900/40 dark:bg-green-700/40"
tabIndex={-1}
onClick={() => setSelectedProduct(null)}
/>
<motion.div
className="relative z-10 grid w-2/3 grid-cols-1 items-stretch rounded-md bg-white text-xs dark:bg-neutral-800 max-lg:h-[45vh] max-sm:max-w-3xl sm:w-auto lg:grid-cols-[2fr,3fr]"
layoutId={selectedProduct.id}
>
<div className="relative min-h-[20vh] overflow-hidden bg-neutral-900 lg:min-h-[30vh]">
<Image
fill
src={selectedProduct.image.url}
alt={selectedProduct.name}
style={{
position: 'absolute',
inset: 0,
height: '100%',
width: '100%',
objectFit: 'cover',
objectPosition: 'center'
}}
/>
</div>
<div className="relative px-3 py-4 pr-4 pt-4">
<button
type="button"
className="absolute right-4 rounded-lg bg-neutral-300 p-2 dark:bg-neutral-700 max-md:bottom-4 md:top-4"
onClick={() => setSelectedProduct(null)}
>
<HiX className="h-4 w-auto text-neutral-900 dark:text-neutral-200" />
</button>
<div className="mx-auto flex h-full w-11/12 flex-col justify-between text-xs max-sm:space-y-8 ">
{/* about */}
<div className="space-y-2">
<div className="line-clamp-1">{selectedProduct.id}</div>
<div className="font-bold uppercase">{selectedProduct.name}</div>
<div className="">$ {selectedProduct.price}</div>
<div className="text-xs">Weight : {selectedProduct.weight} KG</div>
</div>
{/* type */}
<div className="">
<div className="flex items-center justify-between">
<span className="bg-neutral-300 px-2 py-1 font-bold uppercase dark:bg-neutral-700">
{selectedProduct.type}
</span>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</section>
</main>
)
}
Conclusion
We have successfully created a grid with a selected item animation. You can use this animation to create a grid of products, images, or any other content. You can also use this animation to create a grid of cards with a selected item animation.