Background parttern

Grid Card animation with framer motion

A simple layout animation with framer motion

framer-motionreactanimationgridcsslayout

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.

ecdc8405-87a9-4950-872e-885e27311481
Battlefield
$ 88.99
Weight : 0.07 KG
e8fb7251-6d54-46f0-995f-28845fc6cf30
Assasin's Creed
$ 87.99
Weight : 0.08 KG