
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 motion/react a popular animation library for React.
## Setup
### Dependencies Installation
We will install the required dependencies for the project.
npm install motion/react tailwindcss postcss autoprefixer clsx
pnpm add motion/react tailwindcss postcss autoprefixer clsx
yarn add motion/react tailwindcss postcss autoprefixer clsx
bun add motion/react 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.
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.
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.
@tailwind base;
@tailwind components;
@tailwind utilities;
## Creating the Component
We will create a new file animated-grid.tsx
in the components
folder and add the following code.
'use client';
import { useState } from 'react';
import Image from 'next/image';
import { HiX } from 'react-icons/hi';
import { AnimatePresence, motion } from 'motion/react';
interface ProductI {
id: string;
name: string;
price: number;
image: {
url: string;
};
type: string;
weight: number;
description: string;
createdAt: string;
updatedAt: string;
}
const products: ProductI[] = [
{
id: 'ecdc8405-87a9-4950-872e-885e27311481',
name: 'Battlefield',
price: 88.99,
type: 'book',
weight: 0.07,
description: 'Game, Thriller, Action',
image: {
url: '/images/grid-component/540202.jpg'
},
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: '/images/grid-component/320623.png'
},
createdAt: '2023-04-30T18:55:30.687Z',
updatedAt: '2023-04-30T18:55:30.687Z'
}
];
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 size-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 max-lg:h-[45vh] max-sm:max-w-3xl sm:w-auto lg:grid-cols-[2fr,3fr] dark:bg-neutral-800"
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">
<button
type="button"
className="absolute right-4 rounded-lg bg-neutral-300 p-2 max-md:bottom-4 md:top-4 dark:bg-neutral-700"
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.
Published on May 8, 2023
2 min read
Found an Issue!
Find an issue with this post? Think you could clarify, update or add something? All my posts are available to edit on Github. Any fix, little or small, is appreciated!
Edit on GitHub